import { describe, expect, it } from 'vitest';
import * as core from '@/index';
import {
h,
mount,
mountCallback,
nextTick,
render,
updateCallback,
} from '@/index';
const concurrent = 'deferRender' in core;
describe('same-order keyed matching baseline', () => {
it('preserves children getter observations on a reused identical VDOM object', async () => {
let refresh = () => {};
let armed = false;
let reads = 0;
const held = held;
let children = held.children;
Object.defineProperty(held, 'children', {
configurable: true,
get() {
if (armed) reads++;
return children;
},
set(value) {
children = value;
},
});
const App = mount(renew => {
refresh = renew;
return () => ;
});
const host = document.createElement('div');
const dispose = render(, host);
try {
const element = host.querySelector('b');
armed = true;
refresh();
await nextTick();
expect(reads).toBeGreaterThan(0);
expect(host.querySelector('b')).toBe(element);
expect(host.textContent).toBe('held');
} finally {
dispose();
}
});
it('preserves each core baseline when the next array has a hole at the same length', () => {
let sparse = false;
const App = mount(() => {
return () => {
const rows = [a, b, c];
if (sparse) delete rows[1];
return ;
};
});
const host = document.createElement('div');
const tree = ;
const dispose = render(tree, host);
try {
const before = [...host.querySelectorAll('b')];
sparse = true;
const update = () =>
core.replaceWDom(App, tree.compProps!, tree.compChild!, tree);
if (concurrent) {
// Existing concurrent work frames visit array holes as undefined nodes.
// Characterize that limitation; DC-25 does not change this core path.
expect(update).toThrow(TypeError);
expect(host.textContent).toBe('abc');
} else {
update();
expect(host.textContent).toBe('ac');
expect([...host.querySelectorAll('b')]).toEqual([before[0], before[2]]);
}
} finally {
dispose();
}
});
it.each([
[-0, +0],
[NaN, NaN],
])(
'consumes only the first SameValueZero duplicate %j',
async (first, second) => {
let refresh = () => {};
let instance = 0;
let version = 0;
const removed: number[] = [];
const Row = mount<{ key: number; label: number }>((_renew, props) => {
const id = ++instance;
mountCallback(() => () => removed.push(id));
return () => {`${version}:${props.label}`};
});
const App = mount(renew => {
refresh = renew;
return () => (
{[first, second].map((key, label) => (
))}
);
});
const host = document.createElement('div');
const dispose = render(, host);
const original = host.firstElementChild!.firstElementChild;
try {
for (version = 1; version <= 3; version++) {
refresh();
await nextTick();
expect(host.firstElementChild!.firstElementChild).toBe(original);
expect(
[...host.querySelectorAll('b')].map(node =>
node.getAttribute('data-id')
)
).toEqual(['1', String(version + 2)]);
expect(host.textContent).toBe(`${version}:0${version}:1`);
expect(removed).toEqual(
Array.from({ length: version }, (_, i) => i + 2)
);
}
} finally {
dispose();
}
}
);
it('does not replay the consumed prefix when an updater changes later matches', async () => {
let refresh = () => {};
let updating = false;
let nextProps: { key: string; label: string }[] = [];
let instance = 0;
const trace: string[] = [];
const removed: number[] = [];
const Row = mount<{ key: string; label: string }>((_renew, props) => {
const id = ++instance;
updateCallback(() => {
trace.push(`pre:${id}`);
if (updating && props.label === 'a') {
nextProps[1].key = 'a';
nextProps[2].key = 'd';
nextProps[3].key = 'c';
}
});
mountCallback(() => () => removed.push(id));
return () => {props.label};
});
const App = mount(renew => {
refresh = renew;
return () => {
const rows = ['a', 'b', 'c', 'd'].map(label => (
));
nextProps = rows.map(
row => row.props as { key: string; label: string }
);
return ;
};
});
const host = document.createElement('div');
const dispose = render(, host);
try {
updating = true;
refresh();
await nextTick();
expect(host.textContent).toBe('abcd');
expect(trace).toEqual(
concurrent
? ['pre:1', 'pre:2', 'pre:3', 'pre:4']
: ['pre:1', 'pre:4', 'pre:3']
);
expect(
[...host.querySelectorAll('b')].map(node =>
node.getAttribute('data-id')
)
).toEqual(concurrent ? ['1', '2', '3', '4'] : ['1', '5', '4', '3']);
expect(removed).toEqual(concurrent ? [] : [2]);
} finally {
dispose();
}
});
it('reads all original keys before a later incoming key getter throws', () => {
let updating = false;
const trace: string[] = [];
const App = mount(() => () => {
const source = updating ? 'next' : 'old';
return (
{[0, 1, 2].map(id =>
h(
'b',
{
get key() {
if (updating) trace.push(`${source}:${id}`);
if (updating && source === 'next' && id === 1)
throw Error('later key getter');
return id;
},
},
id
)
)}
);
});
const host = document.createElement('div');
const tree = ;
const dispose = render(tree, host);
try {
updating = true;
expect(() =>
core.replaceWDom(App, tree.compProps!, tree.compChild!, tree)
).toThrow('later key getter');
expect(trace.indexOf('old:2')).toBeGreaterThan(-1);
expect(trace.indexOf('next:1')).toBeGreaterThan(trace.indexOf('old:2'));
expect(trace.filter(value => value === 'next:1')).toHaveLength(1);
expect(host.textContent).toBe('012');
} finally {
dispose();
}
});
});