Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { jitSuite, RenderTest, test, tracked } from '@glimmer-workspace/integration-tests';

// More updates than a dropped guard needs to come back.
const MANY = 20;

class Item {
@tracked text: string;

constructor(
public id: number,
text: string
) {
this.text = text;
}
}

class BlockGuardsTest extends RenderTest {
static suiteName = 'Block guards';

@test
'a block updates after many renders that did not change it'() {
this.render('{{#if this.show}}<p>{{this.inner}}</p>{{/if}}<b>{{this.other}}</b>', {
show: true,
inner: 'a',
other: 0,
});
this.assertHTML('<p>a</p><b>0</b>');

for (let i = 1; i <= MANY; i++) {
this.rerender({ other: i });
this.assertHTML(`<p>a</p><b>${i}</b>`);
}

this.rerender({ inner: 'b' });
this.assertHTML(`<p>b</p><b>${MANY}</b>`);
this.assertStableNodes();
}

@test
'a block that changes on every render updates, rests, and updates again'() {
this.render('{{#if this.show}}<p>{{this.inner}}</p>{{/if}}<b>{{this.other}}</b>', {
show: true,
inner: 'a',
other: 0,
});

for (let i = 1; i <= MANY; i++) {
this.rerender({ inner: `v${i}` });
this.assertHTML(`<p>v${i}</p><b>0</b>`);
}

for (let i = 1; i <= MANY; i++) {
this.rerender({ other: i });
this.assertHTML(`<p>v${MANY}</p><b>${i}</b>`);
}

this.rerender({ inner: 'last' });
this.assertHTML(`<p>last</p><b>${MANY}</b>`);
this.assertStableNodes();
}

@test
'a block that renders again on every update keeps the right content'() {
this.render('{{#if this.cond}}<p>yes {{this.text}}</p>{{else}}<p>no {{this.text}}</p>{{/if}}', {
cond: true,
text: 'a',
});
this.assertHTML('<p>yes a</p>');

for (let i = 1; i <= MANY; i++) {
let cond = i % 2 === 0;

this.rerender({ cond, text: `t${i}` });
this.assertHTML(`<p>${cond ? 'yes' : 'no'} t${i}</p>`);
}

for (let i = 1; i <= MANY; i++) {
this.rerender({ text: `u${i}` });
this.assertHTML(`<p>yes u${i}</p>`);
}

this.rerender({ cond: false });
this.assertHTML(`<p>no u${MANY}</p>`);
}

@test
'a nested block updates when the outer block has nothing else to do'() {
this.render(
'{{#if this.outer}}<div>{{#if this.inner}}<p>{{this.text}}</p>{{/if}}</div>{{/if}}<b>{{this.other}}</b>',
{ outer: true, inner: true, text: 'a', other: 0 }
);
this.assertHTML('<div><p>a</p></div><b>0</b>');

for (let i = 1; i <= MANY; i++) {
this.rerender({ other: i });
}

this.rerender({ text: 'b' });
this.assertHTML(`<div><p>b</p></div><b>${MANY}</b>`);

for (let i = 1; i <= MANY; i++) {
this.rerender({ text: `c${i}` });
this.assertHTML(`<div><p>c${i}</p></div><b>${MANY}</b>`);
}

this.rerender({ inner: false });
this.assertHTML(`<div><!----></div><b>${MANY}</b>`);

this.rerender({ inner: true, text: 'd' });
this.assertHTML(`<div><p>d</p></div><b>${MANY}</b>`);
}

@test
'an item of a list updates after many renders that changed another item'() {
let items = [new Item(1, 'a'), new Item(2, 'b'), new Item(3, 'c')];

this.render('{{#each this.items key="id" as |item|}}<p>{{item.text}}</p>{{/each}}', { items });
this.assertHTML('<p>a</p><p>b</p><p>c</p>');

for (let i = 1; i <= MANY; i++) {
items[0]!.text = `a${i}`;
this.rerender();
this.assertHTML(`<p>a${i}</p><p>b</p><p>c</p>`);
}

items[2]!.text = 'z';
this.rerender();
this.assertHTML(`<p>a${MANY}</p><p>b</p><p>z</p>`);
this.assertStableNodes();
}

@test
'a list updates its items after many renders with new arrays'() {
let items = [new Item(1, 'a'), new Item(2, 'b'), new Item(3, 'c')];

this.render('{{#each this.items key="id" as |item|}}<p>{{item.text}}</p>{{/each}}', { items });

for (let i = 1; i <= MANY; i++) {
items = [new Item(1, 'a'), new Item(2, `b${i}`), new Item(3, 'c')];
this.rerender({ items });
this.assertHTML(`<p>a</p><p>b${i}</p><p>c</p>`);
}

items = [new Item(3, 'c'), new Item(1, 'first'), new Item(4, 'new')];
this.rerender({ items });
this.assertHTML('<p>c</p><p>first</p><p>new</p>');
}
}

jitSuite(BlockGuardsTest);
23 changes: 23 additions & 0 deletions packages/@glimmer/runtime/lib/vm/append.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class Stacks {
readonly cache = new Stack<JumpIfNotModifiedOpcode>();
readonly list = new Stack<ListBlockOpcode>();
readonly destroyable = new Stack<object>();
readonly block = new Stack<BlockOpcode>();

constructor(scope: Scope, dynamicScope: DynamicScope) {
this.scope.push(scope);
Expand Down Expand Up @@ -402,6 +403,8 @@ export class VM {
enter(args: number) {
let updating: UpdatingOpcode[] = [];

beginTrackFrame();

let state = this.capture(args);
let block = this.tree().pushResettableBlock();

Expand Down Expand Up @@ -438,6 +441,8 @@ export class VM {
enterItem({ key, value, memo }: OpaqueIterationItem): ListItemOpcode {
let { stack } = this;

beginTrackFrame();

let valueRef = createIteratorItemRef(value);
let memoRef = createIteratorItemRef(memo);

Expand Down Expand Up @@ -479,6 +484,10 @@ export class VM {
enterList(iterableRef: Reference<OpaqueIterator>, offset: number) {
let updating: ListItemOpcode[] = [];

// The list block reads its iterable in the constructor, so the frame must
// be open before that read.
beginTrackFrame();

let addr = this.lowlevel.target(offset);
let state = this.capture(0, addr);
let list = this.tree().pushBlockList(updating) as AppendingBlockList;
Expand Down Expand Up @@ -509,10 +518,23 @@ export class VM {
private didEnter(opcode: BlockOpcode) {
this.associateDestroyable(opcode);
this.#stacks.destroyable.push(opcode);
this.#stacks.block.push(opcode);
this.updateWith(opcode);
this.pushUpdating(opcode.children);
}

/**
* Re-enter a block that the updating VM is re-rendering. Its tracking frame
* is already open, opened by the updating VM before it evaluated the block.
*
* [!] push Block Stack <- `opcode`
* [!] push Updating Stack <- `children`
*/
resumeBlock(opcode: BlockOpcode, children: UpdatingOpcode[]) {
this.#stacks.block.push(opcode);
this.pushUpdating(children);
}

/**
* ## Opcodes
*
Expand All @@ -529,6 +551,7 @@ export class VM {
this.#stacks.destroyable.pop();
this.#tree.popBlock();
this.popUpdating();
expect(this.#stacks.block.pop(), 'VM BUG: expected a block to exit').didExit();
}

/**
Expand Down
Loading
Loading