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.
@@ -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
- export { find, findAll, textContent, attr, getByRole, getAllByRole, getByText, getByPlaceholder, getByLabel, role, placeholder, label, selector, text, within, } from './query';
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 invokeAndCapture = (simulation, target, eventName, invokeHandler) => {
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
- export const click = (target) => (simulation) => invokeAndCapture(simulation, target, 'click', handler => {
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
- assertion(locator(internal.html), locator.description, isNot);
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) === expected,
398
+ pass: textMatches(textContent(vnode), expected),
220
399
  actual: `received "${textContent(vnode)}"`,
221
- }), `have text "${expected}"`);
400
+ }), `have text ${describeExpected(expected)}`);
222
401
  const assertContainsText = (expected) => assertOnElement(vnode => ({
223
- pass: textContent(vnode).includes(expected),
402
+ pass: textIncludes(textContent(vnode), expected),
224
403
  actual: `received "${textContent(vnode)}"`,
225
- }), `contain text "${expected}"`);
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 = steps.reduce((current, step) => {
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":"AAIA,OAAO,QAAQ,QAAQ,CAAC;IAEtB,UAAU,SAAS,CAAC,CAAC;QACnB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;QAClC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;QACrC,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,OAAO,IAAI,IAAI,CAAA;QACf,UAAU,IAAI,IAAI,CAAA;KACnB;CACF"}
1
+ {"version":3,"file":"vitest-setup.d.ts","sourceRoot":"","sources":["../../src/test/vitest-setup.ts"],"names":[],"mappings":""}
@@ -1,3 +1,2 @@
1
- import { expect } from 'vitest';
2
- import { sceneMatchers } from './matchers';
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
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,OAAO,EACR,MAAM,UAAU,CAAA;AAEjB,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,CAAA;AAElB,eAAO,MAAM,KAAK,gIAOhB,CAAA"}
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, propsModule, styleModule, toVNode, } from 'snabbdom';
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.45.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"