foldkit 0.45.0 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/propsModule.d.ts +3 -0
- package/dist/propsModule.d.ts.map +1 -0
- package/dist/propsModule.js +50 -0
- package/dist/test/apps/interactions.d.ts +26 -0
- package/dist/test/apps/interactions.d.ts.map +1 -0
- package/dist/test/apps/interactions.js +59 -0
- package/dist/test/apps/multiRole.d.ts +13 -0
- package/dist/test/apps/multiRole.d.ts.map +1 -0
- package/dist/test/apps/multiRole.js +19 -0
- package/dist/test/matchers.d.ts +26 -2
- package/dist/test/matchers.d.ts.map +1 -1
- package/dist/test/matchers.js +89 -9
- package/dist/test/query.d.ts +111 -11
- package/dist/test/query.d.ts.map +1 -1
- package/dist/test/query.js +262 -33
- package/dist/test/scene.d.ts +77 -8
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +281 -28
- package/dist/test/vitest-setup.d.ts +0 -17
- package/dist/test/vitest-setup.d.ts.map +1 -1
- package/dist/test/vitest-setup.js +2 -3
- package/dist/test/vitest.d.ts +33 -0
- package/dist/test/vitest.d.ts.map +1 -0
- package/dist/test/vitest.js +16 -0
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +2 -1
- package/package.json +5 -1
package/dist/test/scene.js
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
|
-
import { Array, Effect, Function, Option, Predicate, pipe } from 'effect';
|
|
1
|
+
import { Array, Effect, Function, Option, Predicate, String as String_, pipe, } from 'effect';
|
|
2
2
|
import { dual } from 'effect/Function';
|
|
3
3
|
import { Dispatch } from '../runtime';
|
|
4
4
|
import { assertAllCommandsResolved, assertNoUnresolvedCommands, resolveByName, } from './internal';
|
|
5
|
-
import { attr, resolveTarget, textContent } from './query';
|
|
6
|
-
|
|
5
|
+
import { accessibleDescription, accessibleName, ancestorsOf, attr, resolveTarget, selector, textContent, within, } from './query';
|
|
6
|
+
import { allAltText, allDisplayValue, allLabel, allPlaceholder, allRole, allSelector, allTestId, allText, allTitle, } from './query';
|
|
7
|
+
export { find, findAll, textContent, attr, getByRole, getAllByRole, getByText, getByPlaceholder, getByLabel, getByAltText, getByTitle, getByTestId, getByDisplayValue, role, placeholder, label, altText, title, testId, displayValue, selector, text, within, getAllByText, getAllByLabel, getAllByPlaceholder, getAllByAltText, getAllByTitle, getAllByTestId, getAllByDisplayValue, first, last, nth, filter, } from './query';
|
|
8
|
+
/** Multi-match Locator factories. Each returns a `LocatorAll` that resolves
|
|
9
|
+
* to every matching VNode. Convert to a single `Locator` via `first`,
|
|
10
|
+
* `last`, or `nth(n)`, or narrow via `filter`. */
|
|
11
|
+
export const all = {
|
|
12
|
+
role: allRole,
|
|
13
|
+
text: allText,
|
|
14
|
+
label: allLabel,
|
|
15
|
+
placeholder: allPlaceholder,
|
|
16
|
+
altText: allAltText,
|
|
17
|
+
title: allTitle,
|
|
18
|
+
testId: allTestId,
|
|
19
|
+
displayValue: allDisplayValue,
|
|
20
|
+
selector: allSelector,
|
|
21
|
+
};
|
|
7
22
|
export { sceneMatchers } from './matchers';
|
|
8
23
|
const UNINITIALIZED = Symbol('uninitialized');
|
|
9
24
|
const toInternal = (simulation) =>
|
|
10
25
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
11
26
|
simulation;
|
|
27
|
+
const applyScopeToTarget = (scope, target) => Option.match(scope, {
|
|
28
|
+
onNone: () => target,
|
|
29
|
+
onSome: parent => typeof target === 'string'
|
|
30
|
+
? within(parent, selector(target))
|
|
31
|
+
: within(parent, target),
|
|
32
|
+
});
|
|
33
|
+
const applyScopeToLocator = (scope, locator) => Option.match(scope, {
|
|
34
|
+
onNone: () => locator,
|
|
35
|
+
onSome: parent => within(parent, locator),
|
|
36
|
+
});
|
|
37
|
+
const applyScopeToLocatorAll = (scope, locatorAll) => Option.match(scope, {
|
|
38
|
+
onNone: () => locatorAll,
|
|
39
|
+
onSome: parent => {
|
|
40
|
+
const resolve = (html) => Option.match(parent(html), {
|
|
41
|
+
onNone: () => [],
|
|
42
|
+
onSome: locatorAll,
|
|
43
|
+
});
|
|
44
|
+
return Object.assign(resolve, {
|
|
45
|
+
description: `${locatorAll.description} within ${parent.description}`,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
12
49
|
// CAPTURING DISPATCH
|
|
13
50
|
const createCapturingDispatch = () => {
|
|
14
51
|
let capturedMessage;
|
|
@@ -38,18 +75,18 @@ const renderView = (viewFn, model, dispatch) => {
|
|
|
38
75
|
// INTERACTION HELPERS
|
|
39
76
|
const EVENT_NAMES = {
|
|
40
77
|
click: 'OnClick',
|
|
78
|
+
dblclick: 'OnDblClick',
|
|
41
79
|
submit: 'OnSubmit',
|
|
42
80
|
input: 'OnInput',
|
|
81
|
+
change: 'OnChange',
|
|
82
|
+
focus: 'OnFocus',
|
|
83
|
+
blur: 'OnBlur',
|
|
84
|
+
mouseenter: 'OnMouseEnter',
|
|
85
|
+
mouseover: 'OnMouseOver',
|
|
43
86
|
keydown: 'OnKeyDown or OnKeyDownPreventDefault',
|
|
44
87
|
};
|
|
45
|
-
const
|
|
88
|
+
const captureFromElement = (simulation, element, description, eventName, invokeHandler) => {
|
|
46
89
|
const internal = toInternal(simulation);
|
|
47
|
-
const { maybeElement, description } = resolveTarget(internal.html, target);
|
|
48
|
-
if (Option.isNone(maybeElement)) {
|
|
49
|
-
throw new Error(`I could not find an element matching ${description}.\n\n` +
|
|
50
|
-
'Check that your selector matches an element in the current view.');
|
|
51
|
-
}
|
|
52
|
-
const element = maybeElement.value;
|
|
53
90
|
const maybeHandler = Option.fromNullable(element.data?.on?.[eventName]);
|
|
54
91
|
if (Option.isNone(maybeHandler)) {
|
|
55
92
|
const attributeName = EVENT_NAMES[eventName] ?? eventName;
|
|
@@ -78,6 +115,46 @@ const invokeAndCapture = (simulation, target, eventName, invokeHandler) => {
|
|
|
78
115
|
};
|
|
79
116
|
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
80
117
|
};
|
|
118
|
+
const invokeAndCapture = (simulation, target, eventName, invokeHandler) => {
|
|
119
|
+
const internal = toInternal(simulation);
|
|
120
|
+
const scopedTarget = applyScopeToTarget(internal.scope, target);
|
|
121
|
+
const { maybeElement, description } = resolveTarget(internal.html, scopedTarget);
|
|
122
|
+
if (Option.isNone(maybeElement)) {
|
|
123
|
+
throw new Error(`I could not find an element matching ${description}.\n\n` +
|
|
124
|
+
'Check that your selector matches an element in the current view.');
|
|
125
|
+
}
|
|
126
|
+
return captureFromElement(simulation, maybeElement.value, description, eventName, invokeHandler);
|
|
127
|
+
};
|
|
128
|
+
const lookupTypeAttribute = (vnode) => {
|
|
129
|
+
const fromAttrs = vnode.data?.attrs?.['type'];
|
|
130
|
+
const fromProps = vnode.data?.props?.['type'];
|
|
131
|
+
return typeof fromAttrs === 'string'
|
|
132
|
+
? fromAttrs
|
|
133
|
+
: typeof fromProps === 'string'
|
|
134
|
+
? fromProps
|
|
135
|
+
: undefined;
|
|
136
|
+
};
|
|
137
|
+
const isSubmitButton = (element) => {
|
|
138
|
+
const type = lookupTypeAttribute(element);
|
|
139
|
+
if (element.sel === 'button') {
|
|
140
|
+
return type === undefined || type === 'submit';
|
|
141
|
+
}
|
|
142
|
+
if (element.sel === 'input') {
|
|
143
|
+
return type === 'submit' || type === 'image';
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
};
|
|
147
|
+
const isElementDisabled = (element) => {
|
|
148
|
+
const attrDisabled = element.data?.attrs?.['disabled'];
|
|
149
|
+
const propDisabled = element.data?.props?.['disabled'];
|
|
150
|
+
const ariaDisabled = element.data?.attrs?.['aria-disabled'];
|
|
151
|
+
return (attrDisabled === true ||
|
|
152
|
+
attrDisabled === '' ||
|
|
153
|
+
attrDisabled === 'disabled' ||
|
|
154
|
+
propDisabled === true ||
|
|
155
|
+
ariaDisabled === 'true' ||
|
|
156
|
+
ariaDisabled === true);
|
|
157
|
+
};
|
|
81
158
|
const DEFAULT_KEYBOARD_MODIFIERS = {
|
|
82
159
|
shiftKey: false,
|
|
83
160
|
ctrlKey: false,
|
|
@@ -155,11 +232,109 @@ export const tap = (f) => (simulation) => {
|
|
|
155
232
|
f(simulation);
|
|
156
233
|
return simulation;
|
|
157
234
|
};
|
|
235
|
+
const runSteps = (seed, steps) =>
|
|
236
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
237
|
+
Array.reduce(steps, seed, (current, step) => {
|
|
238
|
+
const next = step(current);
|
|
239
|
+
const internal = toInternal(next);
|
|
240
|
+
if (internal.model !== UNINITIALIZED) {
|
|
241
|
+
const html = renderView(internal.viewFn, internal.model, internal.capturingDispatch.dispatch);
|
|
242
|
+
return { ...internal, html };
|
|
243
|
+
}
|
|
244
|
+
return next;
|
|
245
|
+
});
|
|
246
|
+
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
247
|
+
/** Scopes a sequence of steps to a parent element. Every Locator referenced by
|
|
248
|
+
* child steps — assertions, interactions — resolves within the parent's subtree.
|
|
249
|
+
* Use this when several steps share the same scope. For a single scoped query,
|
|
250
|
+
* prefer `within(parent, child)` directly. Nested `inside` calls compose scopes
|
|
251
|
+
* via `within(outer, inner)`. */
|
|
252
|
+
export const inside = (parent, ...steps) => (simulation) => {
|
|
253
|
+
const internal = toInternal(simulation);
|
|
254
|
+
const priorScope = internal.scope;
|
|
255
|
+
const nextScope = Option.match(priorScope, {
|
|
256
|
+
onNone: () => parent,
|
|
257
|
+
onSome: within(parent),
|
|
258
|
+
});
|
|
259
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
260
|
+
const scopedEntry = {
|
|
261
|
+
...internal,
|
|
262
|
+
scope: Option.some(nextScope),
|
|
263
|
+
};
|
|
264
|
+
const afterSteps = runSteps(scopedEntry, steps);
|
|
265
|
+
const afterInternal = toInternal(afterSteps);
|
|
266
|
+
return {
|
|
267
|
+
...afterInternal,
|
|
268
|
+
scope: priorScope,
|
|
269
|
+
};
|
|
270
|
+
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
271
|
+
};
|
|
158
272
|
// INTERACTION STEPS
|
|
159
|
-
/** Simulates a click on the element matching the target.
|
|
160
|
-
|
|
273
|
+
/** Simulates a click on the element matching the target.
|
|
274
|
+
* When the element is a submit button (`<button>` with no type or
|
|
275
|
+
* `type="submit"`, `<input type="submit">`, `<input type="image">`) with no
|
|
276
|
+
* click handler of its own, the click falls through to the `submit` handler
|
|
277
|
+
* of the nearest ancestor `<form>` — mirroring browser behavior. */
|
|
278
|
+
export const click = (target) => (simulation) => {
|
|
279
|
+
const internal = toInternal(simulation);
|
|
280
|
+
const scopedTarget = applyScopeToTarget(internal.scope, target);
|
|
281
|
+
const { maybeElement, description } = resolveTarget(internal.html, scopedTarget);
|
|
282
|
+
if (Option.isNone(maybeElement)) {
|
|
283
|
+
throw new Error(`I could not find an element matching ${description}.\n\n` +
|
|
284
|
+
'Check that your selector matches an element in the current view.');
|
|
285
|
+
}
|
|
286
|
+
const element = maybeElement.value;
|
|
287
|
+
if (isElementDisabled(element)) {
|
|
288
|
+
throw new Error(`I found an element matching ${description} but it is disabled.\n\n` +
|
|
289
|
+
'Disabled elements do not receive click events in the browser. ' +
|
|
290
|
+
'Assert the state that enables the element before clicking, or ' +
|
|
291
|
+
'use Scene.expect(locator).not.toBeDisabled() to verify the ' +
|
|
292
|
+
'element is interactive.');
|
|
293
|
+
}
|
|
294
|
+
const hasClickHandler = element.data?.on?.['click'] !== undefined;
|
|
295
|
+
if (!hasClickHandler && isSubmitButton(element)) {
|
|
296
|
+
const maybeForm = pipe(ancestorsOf(internal.html, element), Array.findLast(vnode => vnode.sel === 'form'));
|
|
297
|
+
if (Option.isSome(maybeForm)) {
|
|
298
|
+
return captureFromElement(simulation, maybeForm.value, `form containing ${description}`, 'submit', handler => {
|
|
299
|
+
handler({ preventDefault: Function.constVoid });
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return captureFromElement(simulation, element, description, 'click', handler => {
|
|
304
|
+
handler();
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
/** Simulates a double-click on the element matching the target. */
|
|
308
|
+
export const doubleClick = (target) => (simulation) => invokeAndCapture(simulation, target, 'dblclick', handler => {
|
|
309
|
+
handler();
|
|
310
|
+
});
|
|
311
|
+
/** Simulates a hover (mouseenter) on the element matching the target.
|
|
312
|
+
* Dispatches the `mouseenter` handler, falling back to `mouseover`. */
|
|
313
|
+
export const hover = (target) => (simulation) => {
|
|
314
|
+
const internal = toInternal(simulation);
|
|
315
|
+
const scopedTarget = applyScopeToTarget(internal.scope, target);
|
|
316
|
+
const { maybeElement } = resolveTarget(internal.html, scopedTarget);
|
|
317
|
+
const eventName = Option.match(maybeElement, {
|
|
318
|
+
onNone: () => 'mouseenter',
|
|
319
|
+
onSome: element => element.data?.on?.['mouseenter'] ? 'mouseenter' : 'mouseover',
|
|
320
|
+
});
|
|
321
|
+
return invokeAndCapture(simulation, target, eventName, handler => {
|
|
322
|
+
handler();
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
/** Simulates a focus event on the element matching the target. */
|
|
326
|
+
export const focus = (target) => (simulation) => invokeAndCapture(simulation, target, 'focus', handler => {
|
|
161
327
|
handler();
|
|
162
328
|
});
|
|
329
|
+
/** Simulates a blur event on the element matching the target. */
|
|
330
|
+
export const blur = (target) => (simulation) => invokeAndCapture(simulation, target, 'blur', handler => {
|
|
331
|
+
handler();
|
|
332
|
+
});
|
|
333
|
+
/** Simulates a change event on the element matching the target.
|
|
334
|
+
* Dual: `change(target, value)` or `change(value)` for data-last piping. */
|
|
335
|
+
export const change = dual(2, (target, value) => (simulation) => invokeAndCapture(simulation, target, 'change', handler => {
|
|
336
|
+
handler({ target: { value } });
|
|
337
|
+
}));
|
|
163
338
|
/** Simulates form submission on the element matching the target. */
|
|
164
339
|
export const submit = (target) => (simulation) => invokeAndCapture(simulation, target, 'submit', handler => {
|
|
165
340
|
handler({ preventDefault: Function.constVoid });
|
|
@@ -182,17 +357,18 @@ export const keydown = dual((args) => args.length >= 2 && typeof args[1] === 'st
|
|
|
182
357
|
}));
|
|
183
358
|
const wrapAssertion = (locator, assertion, isNot) => (simulation) => {
|
|
184
359
|
const internal = toInternal(simulation);
|
|
185
|
-
|
|
360
|
+
const scopedLocator = applyScopeToLocator(internal.scope, locator);
|
|
361
|
+
assertion(scopedLocator(internal.html), scopedLocator.description, isNot, internal.html);
|
|
186
362
|
return simulation;
|
|
187
363
|
};
|
|
188
|
-
const assertOnElement = (check, expectation) => (maybeElement, description, isNot) => {
|
|
364
|
+
const assertOnElement = (check, expectation) => (maybeElement, description, isNot, root) => {
|
|
189
365
|
if (Option.isNone(maybeElement)) {
|
|
190
366
|
if (!isNot) {
|
|
191
367
|
throw new Error(`Expected element matching ${description} to ${expectation} but the element does not exist.`);
|
|
192
368
|
}
|
|
193
369
|
return;
|
|
194
370
|
}
|
|
195
|
-
const { pass, actual } = check(maybeElement.value);
|
|
371
|
+
const { pass, actual } = check(maybeElement.value, root);
|
|
196
372
|
if (isNot ? pass : !pass) {
|
|
197
373
|
throw new Error(isNot
|
|
198
374
|
? `Expected element matching ${description} not to ${expectation} but it does.`
|
|
@@ -215,14 +391,17 @@ const assertAbsent = (maybeElement, description, isNot) => {
|
|
|
215
391
|
: `Expected element matching ${description} to be absent but it exists.`);
|
|
216
392
|
}
|
|
217
393
|
};
|
|
394
|
+
const describeExpected = (expected) => expected instanceof RegExp ? `${expected}` : `"${expected}"`;
|
|
395
|
+
const textMatches = (value, expected) => expected instanceof RegExp ? expected.test(value) : value === expected;
|
|
396
|
+
const textIncludes = (value, expected) => expected instanceof RegExp ? expected.test(value) : value.includes(expected);
|
|
218
397
|
const assertHasText = (expected) => assertOnElement(vnode => ({
|
|
219
|
-
pass: textContent(vnode)
|
|
398
|
+
pass: textMatches(textContent(vnode), expected),
|
|
220
399
|
actual: `received "${textContent(vnode)}"`,
|
|
221
|
-
}), `have text
|
|
400
|
+
}), `have text ${describeExpected(expected)}`);
|
|
222
401
|
const assertContainsText = (expected) => assertOnElement(vnode => ({
|
|
223
|
-
pass: textContent(vnode)
|
|
402
|
+
pass: textIncludes(textContent(vnode), expected),
|
|
224
403
|
actual: `received "${textContent(vnode)}"`,
|
|
225
|
-
}), `contain text
|
|
404
|
+
}), `contain text ${describeExpected(expected)}`);
|
|
226
405
|
const assertHasAttr = (name, value) => assertOnElement(vnode => {
|
|
227
406
|
const actualValue = attr(vnode, name);
|
|
228
407
|
if (Predicate.isUndefined(value)) {
|
|
@@ -314,6 +493,56 @@ const assertIsChecked = assertOnElement(vnode => {
|
|
|
314
493
|
(Option.isSome(ariaChecked) && ariaChecked.value === 'true');
|
|
315
494
|
return { pass, actual: 'it is not checked' };
|
|
316
495
|
}, 'be checked');
|
|
496
|
+
const isHidden = (vnode) => {
|
|
497
|
+
const hiddenAttr = attr(vnode, 'hidden');
|
|
498
|
+
if (Option.isSome(hiddenAttr) && hiddenAttr.value !== 'false')
|
|
499
|
+
return true;
|
|
500
|
+
const ariaHidden = attr(vnode, 'aria-hidden');
|
|
501
|
+
if (Option.isSome(ariaHidden) && ariaHidden.value === 'true')
|
|
502
|
+
return true;
|
|
503
|
+
const display = vnode.data?.style?.['display'];
|
|
504
|
+
if (display === 'none')
|
|
505
|
+
return true;
|
|
506
|
+
const visibility = vnode.data?.style?.['visibility'];
|
|
507
|
+
if (visibility === 'hidden')
|
|
508
|
+
return true;
|
|
509
|
+
return false;
|
|
510
|
+
};
|
|
511
|
+
const assertIsVisible = assertOnElement(vnode => ({ pass: !isHidden(vnode), actual: 'it is hidden' }), 'be visible');
|
|
512
|
+
const assertHasAccessibleName = (expected) => assertOnElement((vnode, root) => {
|
|
513
|
+
const actual = accessibleName(root)(vnode);
|
|
514
|
+
return {
|
|
515
|
+
pass: textMatches(actual, expected),
|
|
516
|
+
actual: `received "${actual}"`,
|
|
517
|
+
};
|
|
518
|
+
}, `have accessible name ${describeExpected(expected)}`);
|
|
519
|
+
const assertHasAccessibleDescription = (expected) => assertOnElement((vnode, root) => {
|
|
520
|
+
const actual = accessibleDescription(root)(vnode);
|
|
521
|
+
return {
|
|
522
|
+
pass: textMatches(actual, expected),
|
|
523
|
+
actual: `received "${actual}"`,
|
|
524
|
+
};
|
|
525
|
+
}, `have accessible description ${describeExpected(expected)}`);
|
|
526
|
+
const assertIsEmpty = assertOnElement(vnode => {
|
|
527
|
+
const childCount = (vnode.children ?? []).length;
|
|
528
|
+
const text = textContent(vnode);
|
|
529
|
+
return {
|
|
530
|
+
pass: String_.isEmpty(text) && childCount === 0,
|
|
531
|
+
actual: String_.isNonEmpty(text)
|
|
532
|
+
? `received text "${text}"`
|
|
533
|
+
: `received ${childCount} child(ren)`,
|
|
534
|
+
};
|
|
535
|
+
}, 'be empty');
|
|
536
|
+
const assertHasId = (expected) => assertOnElement(vnode => {
|
|
537
|
+
const actualId = attr(vnode, 'id');
|
|
538
|
+
return Option.match(actualId, {
|
|
539
|
+
onNone: () => ({ pass: false, actual: 'the element has no id' }),
|
|
540
|
+
onSome: actual => ({
|
|
541
|
+
pass: actual === expected,
|
|
542
|
+
actual: `received "${actual}"`,
|
|
543
|
+
}),
|
|
544
|
+
});
|
|
545
|
+
}, `have id "${expected}"`);
|
|
317
546
|
const buildExpectChain = (locator, isNot) => ({
|
|
318
547
|
toExist: () => wrapAssertion(locator, assertExists, isNot),
|
|
319
548
|
toBeAbsent: () => wrapAssertion(locator, assertAbsent, isNot),
|
|
@@ -327,6 +556,11 @@ const buildExpectChain = (locator, isNot) => ({
|
|
|
327
556
|
toHaveValue: (expected) => wrapAssertion(locator, assertHasValue(expected), isNot),
|
|
328
557
|
toBeDisabled: () => wrapAssertion(locator, assertIsDisabled, isNot),
|
|
329
558
|
toBeEnabled: () => wrapAssertion(locator, assertIsEnabled, isNot),
|
|
559
|
+
toBeEmpty: () => wrapAssertion(locator, assertIsEmpty, isNot),
|
|
560
|
+
toBeVisible: () => wrapAssertion(locator, assertIsVisible, isNot),
|
|
561
|
+
toHaveId: (expected) => wrapAssertion(locator, assertHasId(expected), isNot),
|
|
562
|
+
toHaveAccessibleName: (expected) => wrapAssertion(locator, assertHasAccessibleName(expected), isNot),
|
|
563
|
+
toHaveAccessibleDescription: (expected) => wrapAssertion(locator, assertHasAccessibleDescription(expected), isNot),
|
|
330
564
|
toBeChecked: () => wrapAssertion(locator, assertIsChecked, isNot),
|
|
331
565
|
});
|
|
332
566
|
/** Creates an inline assertion step. Resolves the Locator against
|
|
@@ -336,6 +570,32 @@ const expect_ = (locator) => ({
|
|
|
336
570
|
...buildExpectChain(locator, false),
|
|
337
571
|
not: buildExpectChain(locator, true),
|
|
338
572
|
});
|
|
573
|
+
// LOCATOR-ALL ASSERTIONS
|
|
574
|
+
const wrapAllAssertion = (locatorAll, assertion, isNot) => (simulation) => {
|
|
575
|
+
const internal = toInternal(simulation);
|
|
576
|
+
const scopedLocatorAll = applyScopeToLocatorAll(internal.scope, locatorAll);
|
|
577
|
+
assertion(scopedLocatorAll(internal.html), scopedLocatorAll.description, isNot);
|
|
578
|
+
return simulation;
|
|
579
|
+
};
|
|
580
|
+
const assertCount = (expected) => (matches, description, isNot) => {
|
|
581
|
+
const actual = matches.length;
|
|
582
|
+
const pass = actual === expected;
|
|
583
|
+
if (isNot ? pass : !pass) {
|
|
584
|
+
throw new Error(isNot
|
|
585
|
+
? `Expected elements matching ${description} not to have count ${expected} but they do.`
|
|
586
|
+
: `Expected elements matching ${description} to have count ${expected} but received ${actual}.`);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const buildExpectAllChain = (locatorAll, isNot) => ({
|
|
590
|
+
toHaveCount: (expected) => wrapAllAssertion(locatorAll, assertCount(expected), isNot),
|
|
591
|
+
toBeEmpty: () => wrapAllAssertion(locatorAll, assertCount(0), isNot),
|
|
592
|
+
});
|
|
593
|
+
/** Creates an inline multi-match assertion step. Use for count-based
|
|
594
|
+
* assertions like `toHaveCount(n)` or `toBeEmpty()`. */
|
|
595
|
+
export const expectAll = (locatorAll) => ({
|
|
596
|
+
...buildExpectAllChain(locatorAll, false),
|
|
597
|
+
not: buildExpectAllChain(locatorAll, true),
|
|
598
|
+
});
|
|
339
599
|
// SUBMODEL VIEW ADAPTER
|
|
340
600
|
/** Adapts a submodel view for Scene testing. In the Submodel pattern, the view
|
|
341
601
|
* takes a `toParentMessage` function that maps child Messages to parent Messages.
|
|
@@ -357,16 +617,9 @@ export const scene = (config, ...steps) => {
|
|
|
357
617
|
html: undefined,
|
|
358
618
|
viewFn: config.view,
|
|
359
619
|
capturingDispatch,
|
|
620
|
+
scope: Option.none(),
|
|
360
621
|
};
|
|
361
|
-
const result =
|
|
362
|
-
const next = step(current);
|
|
363
|
-
const internal = toInternal(next);
|
|
364
|
-
if (internal.model !== UNINITIALIZED) {
|
|
365
|
-
const html = renderView(internal.viewFn, internal.model, internal.capturingDispatch.dispatch);
|
|
366
|
-
return { ...internal, html };
|
|
367
|
-
}
|
|
368
|
-
return next;
|
|
369
|
-
}, seed);
|
|
622
|
+
const result = runSteps(seed, steps);
|
|
370
623
|
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
371
624
|
const internal = toInternal(result);
|
|
372
625
|
assertAllCommandsResolved(internal.commands);
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
declare module 'vitest' {
|
|
2
|
-
interface Assertion<T> {
|
|
3
|
-
toHaveText(expected: string): this;
|
|
4
|
-
toContainText(expected: string): this;
|
|
5
|
-
toHaveClass(expected: string): this;
|
|
6
|
-
toHaveAttr(name: string, value?: string): this;
|
|
7
|
-
toHaveStyle(name: string, value?: string): this;
|
|
8
|
-
toHaveHook(name: string): this;
|
|
9
|
-
toHaveHandler(name: string): this;
|
|
10
|
-
toHaveValue(expected: string): this;
|
|
11
|
-
toBeDisabled(): this;
|
|
12
|
-
toBeEnabled(): this;
|
|
13
|
-
toBeChecked(): this;
|
|
14
|
-
toExist(): this;
|
|
15
|
-
toBeAbsent(): this;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
1
|
export {};
|
|
19
2
|
//# sourceMappingURL=vitest-setup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-setup.d.ts","sourceRoot":"","sources":["../../src/test/vitest-setup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vitest-setup.d.ts","sourceRoot":"","sources":["../../src/test/vitest-setup.ts"],"names":[],"mappings":""}
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
expect.extend(sceneMatchers);
|
|
1
|
+
import { setup } from './vitest';
|
|
2
|
+
setup();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare module 'vitest' {
|
|
2
|
+
interface Assertion<T> {
|
|
3
|
+
toHaveText(expected: string | RegExp): this;
|
|
4
|
+
toContainText(expected: string | RegExp): this;
|
|
5
|
+
toHaveClass(expected: string): this;
|
|
6
|
+
toHaveAttr(name: string, value?: string): this;
|
|
7
|
+
toHaveStyle(name: string, value?: string): this;
|
|
8
|
+
toHaveHook(name: string): this;
|
|
9
|
+
toHaveHandler(name: string): this;
|
|
10
|
+
toHaveValue(expected: string): this;
|
|
11
|
+
toBeDisabled(): this;
|
|
12
|
+
toBeEnabled(): this;
|
|
13
|
+
toBeChecked(): this;
|
|
14
|
+
toBeEmpty(): this;
|
|
15
|
+
toBeVisible(): this;
|
|
16
|
+
toHaveId(expected: string): this;
|
|
17
|
+
toExist(): this;
|
|
18
|
+
toBeAbsent(): this;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Registers Foldkit's Scene matchers with Vitest's `expect`.
|
|
22
|
+
* Call once from your Vitest setup file:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // vitest-setup.ts
|
|
26
|
+
* import { setup } from 'foldkit/test/vitest'
|
|
27
|
+
* setup()
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Importing this module also augments `Assertion<T>` with the Scene
|
|
31
|
+
* matcher types — no manual `declare module 'vitest'` block needed. */
|
|
32
|
+
export declare const setup: () => void;
|
|
33
|
+
//# sourceMappingURL=vitest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../../src/test/vitest.ts"],"names":[],"mappings":"AAIA,OAAO,QAAQ,QAAQ,CAAC;IAEtB,UAAU,SAAS,CAAC,CAAC;QACnB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;QAC3C,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;QAC9C,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;QACnC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC/C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QACjC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;QACnC,YAAY,IAAI,IAAI,CAAA;QACpB,WAAW,IAAI,IAAI,CAAA;QACnB,WAAW,IAAI,IAAI,CAAA;QACnB,SAAS,IAAI,IAAI,CAAA;QACjB,WAAW,IAAI,IAAI,CAAA;QACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;QAChC,OAAO,IAAI,IAAI,CAAA;QACf,UAAU,IAAI,IAAI,CAAA;KACnB;CACF;AAED;;;;;;;;;;wEAUwE;AACxE,eAAO,MAAM,KAAK,QAAO,IAExB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
import { sceneMatchers } from './matchers';
|
|
3
|
+
/** Registers Foldkit's Scene matchers with Vitest's `expect`.
|
|
4
|
+
* Call once from your Vitest setup file:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // vitest-setup.ts
|
|
8
|
+
* import { setup } from 'foldkit/test/vitest'
|
|
9
|
+
* setup()
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Importing this module also augments `Assertion<T>` with the Scene
|
|
13
|
+
* matcher types — no manual `declare module 'vitest'` block needed. */
|
|
14
|
+
export const setup = () => {
|
|
15
|
+
expect.extend(sceneMatchers);
|
|
16
|
+
};
|
package/dist/vdom.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,OAAO,EACR,MAAM,UAAU,CAAA;AAIjB,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,CAAA;AAElB,eAAO,MAAM,KAAK,gIAOhB,CAAA"}
|
package/dist/vdom.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { attributesModule, classModule, datasetModule, eventListenersModule, init,
|
|
1
|
+
import { attributesModule, classModule, datasetModule, eventListenersModule, init, styleModule, toVNode, } from 'snabbdom';
|
|
2
|
+
import { propsModule } from './propsModule';
|
|
2
3
|
export { toVNode };
|
|
3
4
|
export const patch = init([
|
|
4
5
|
attributesModule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "A frontend framework for TypeScript, built on Effect, using The Elm Architecture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
"types": "./dist/test/public.d.ts",
|
|
64
64
|
"import": "./dist/test/public.js"
|
|
65
65
|
},
|
|
66
|
+
"./test/vitest": {
|
|
67
|
+
"types": "./dist/test/vitest.d.ts",
|
|
68
|
+
"import": "./dist/test/vitest.js"
|
|
69
|
+
},
|
|
66
70
|
"./ui": {
|
|
67
71
|
"types": "./dist/ui/index.d.ts",
|
|
68
72
|
"import": "./dist/ui/index.js"
|