Skip to content
Merged
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
21 changes: 3 additions & 18 deletions packages/skill-tests/src/formio-form/calculated-values.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,13 @@
// plugin/skills/formio-form/references/calculated-values.md — a JSON Logic
// `calculateValue` deriving a total from quantity × price.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { Formio } from '@formio/js';
import { describe, expect, it, vi } from 'vitest';
import { calculatedFormDefinition } from './fixtures/logic';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
});
import { createForm } from './renderer-harness';

describe('calculated-values.md — calculateValue with JSON Logic', () => {
it('computes the total from quantity and price as inputs change', async () => {
const form = await Formio.createForm(container(), calculatedFormDefinition);
const form = await createForm(calculatedFormDefinition);
expect(form.getComponent('total')).toBeTruthy();

form.getComponent('quantity').setValue(3);
Expand Down
23 changes: 4 additions & 19 deletions packages/skill-tests/src/formio-form/conditionals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,13 @@
// Behavior tests for plugin/skills/formio-form/references/conditionals.md —
// simple (`show`/`when`/`eq`) and JSON Logic (`conditional.json`) visibility.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { Formio } from '@formio/js';
import { describe, expect, it, vi } from 'vitest';
import { jsonConditionalFormDefinition, simpleConditionalFormDefinition } from './fixtures/logic';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
});
import { createForm } from './renderer-harness';

describe('conditionals.md — conditional.json', () => {
it('hides the dependent component until the driver value matches', async () => {
const form = await Formio.createForm(container(), jsonConditionalFormDefinition);
const form = await createForm(jsonConditionalFormDefinition);
const employer = form.getComponent('employer');
expect(employer).toBeTruthy();
expect(employer.visible).toBe(false);
Expand All @@ -42,7 +27,7 @@ describe('conditionals.md — conditional.json', () => {

describe('conditionals.md — simple show/when/eq', () => {
it('behaves the same as the JSON Logic form', async () => {
const form = await Formio.createForm(container(), simpleConditionalFormDefinition);
const form = await createForm(simpleConditionalFormDefinition);
const employer = form.getComponent('employer');
expect(employer).toBeTruthy();
expect(employer.visible).toBe(false);
Expand Down
16 changes: 2 additions & 14 deletions packages/skill-tests/src/formio-form/external-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,10 @@
// fetch is stubbed; no live server involved.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { Formio } from '@formio/js';
import { externalDataFormDefinition } from './fixtures/wizard-external';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}
import { createForm } from './renderer-harness';

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
vi.unstubAllGlobals();
});

Expand All @@ -35,7 +23,7 @@ describe('external-data.md — load external data into the submission', () => {
)
);

const form = await Formio.createForm(container(), externalDataFormDefinition);
const form = await createForm(externalDataFormDefinition);

// The doc example: fetch the profile, then set it as the submission.
const response = await fetch('https://api.example.com/profile/42');
Expand Down
21 changes: 3 additions & 18 deletions packages/skill-tests/src/formio-form/field-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,13 @@
// Behavior tests for plugin/skills/formio-form/references/field-logic.md —
// a component `logic` entry with a `json` trigger applying a property action.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { Formio } from '@formio/js';
import { describe, expect, it, vi } from 'vitest';
import { fieldLogicFormDefinition } from './fixtures/logic';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
});
import { createForm } from './renderer-harness';

describe('field-logic.md — json trigger with a property action', () => {
it('disables the component while the trigger evaluates true', async () => {
const form = await Formio.createForm(container(), fieldLogicFormDefinition);
const form = await createForm(fieldLogicFormDefinition);
const notes = form.getComponent('notes');
expect(notes).toBeTruthy();
expect(notes.disabled).toBeFalsy();
Expand Down
52 changes: 52 additions & 0 deletions packages/skill-tests/src/formio-form/renderer-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Shared jsdom harness for the tests that run the real @formio/js renderer.
//
// Why this exists: `Formio.createForm` leaves a live form instance behind, and
// the renderer finishes work on callbacks that outlive the `it()` that started
// them — validation, error classes, redraws. Removing the container element is
// not enough. A callback that lands after Vitest tears the environment down
// throws `ReferenceError: HTMLElement is not defined` as an *unhandled* error,
// which fails the whole run while every individual test still reports as
// passing, and it only reproduces under some timings (a CI flake).
//
// So: every renderer test creates its form through `createForm` here, and the
// shared `afterEach` destroys the instances and lets the queued work drain
// while the DOM is still alive.

import { afterEach } from 'vitest';
import { Formio } from '@formio/js';

// `Formio.createForm` is declared as `Promise<any>` in @formio/js — the runtime
// instance is loosely typed there (which is why the test files carry
// `@ts-nocheck`). This harness only ever calls `destroy`, so it names that one
// member and leaves the rest opaque instead of spreading `any` further.
type RenderedForm = { destroy: (all?: boolean) => void } & Record<string, unknown>;

const containers: HTMLElement[] = [];
const forms: RenderedForm[] = [];

/**
* Renders a form definition into a fresh container attached to `document.body`,
* tracking both for teardown. Same argument order as `Formio.createForm` minus
* the element, which the harness owns.
*/
export async function createForm(definition: unknown, options?: unknown): Promise<RenderedForm> {
const element = document.createElement('div');
document.body.appendChild(element);
containers.push(element);

const form: RenderedForm = await Formio.createForm(element, definition, options);
forms.push(form);
return form;
}

afterEach(async () => {
for (const form of forms.splice(0)) {
form.destroy(true);
}
// Let anything the renderer queued before `destroy()` run while the DOM still
// exists, rather than after the environment is gone.
await new Promise((resolve) => setTimeout(resolve, 0));
for (const element of containers.splice(0)) {
element.remove();
}
});
29 changes: 7 additions & 22 deletions packages/skill-tests/src/formio-form/rendering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,13 @@
// plugin/skills/formio-form/references/rendering.md, javascript-api.md, and
// options.md — run against the real @formio/js renderer in jsdom.

import { afterEach, describe, expect, it } from 'vitest';
import { Formio } from '@formio/js';
import { describe, expect, it } from 'vitest';
import { contactFormDefinition, prefillSubmission } from './fixtures/rendering';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
});
import { createForm } from './renderer-harness';

describe('rendering.md — Formio.createForm with inline JSON', () => {
it('resolves to a form instance exposing the documented components', async () => {
const form = await Formio.createForm(container(), contactFormDefinition);
const form = await createForm(contactFormDefinition);
expect(form).toBeTruthy();
expect(form.getComponent('firstName')).toBeTruthy();
expect(form.getComponent('lastName')).toBeTruthy();
Expand All @@ -35,7 +20,7 @@ describe('rendering.md — Formio.createForm with inline JSON', () => {

describe('rendering.md — submission pre-fill', () => {
it('round-trips pre-filled data through form.submission', async () => {
const form = await Formio.createForm(container(), contactFormDefinition);
const form = await createForm(contactFormDefinition);
await form.setSubmission(prefillSubmission);
expect(form.submission.data.firstName).toBe('Jane');
const firstName = form.getComponent('firstName');
Expand All @@ -46,7 +31,7 @@ describe('rendering.md — submission pre-fill', () => {

describe('javascript-api.md — events', () => {
it('emits change when a component value is set', async () => {
const form = await Formio.createForm(container(), contactFormDefinition);
const form = await createForm(contactFormDefinition);
const changed = new Promise((resolve) => {
form.on('change', resolve);
});
Expand All @@ -58,7 +43,7 @@ describe('javascript-api.md — events', () => {
});

it('delivers the submission to the submit handler', async () => {
const form = await Formio.createForm(container(), contactFormDefinition);
const form = await createForm(contactFormDefinition);
await form.setSubmission(prefillSubmission);
const submitted = new Promise((resolve) => {
form.on('submit', resolve);
Expand All @@ -71,7 +56,7 @@ describe('javascript-api.md — events', () => {

describe('options.md — renderer options', () => {
it('readOnly: true renders a non-editable form', async () => {
const form = await Formio.createForm(container(), contactFormDefinition, {
const form = await createForm(contactFormDefinition, {
readOnly: true,
});
expect(form.options.readOnly).toBe(true);
Expand Down
23 changes: 4 additions & 19 deletions packages/skill-tests/src/formio-form/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,21 @@
// the canonical `validate.json` contract: JSON Logic evaluating to `true`
// means valid, evaluating to a string makes that string the error message.

import { afterEach, describe, expect, it } from 'vitest';
import { Formio } from '@formio/js';
import { describe, expect, it } from 'vitest';
import { validationFormDefinition } from './fixtures/logic';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
});
import { createForm } from './renderer-harness';

describe('validation.md — validate.json', () => {
it('surfaces the JSON Logic error string for an invalid value', async () => {
const form = await Formio.createForm(container(), validationFormDefinition);
const form = await createForm(validationFormDefinition);
await form.setSubmission({ data: { name: 'Alice' } });
await expect(form.submit()).rejects.toBeTruthy();
const messages = form.errors.map((error) => error.message ?? String(error));
expect(messages.join('\n')).toContain("Your name must be 'Bob'!");
});

it('accepts the valid value and clears the error', async () => {
const form = await Formio.createForm(container(), validationFormDefinition);
const form = await createForm(validationFormDefinition);
const nameComponent = form.getComponent('name');
expect(nameComponent).toBeTruthy();
await form.setSubmission({ data: { name: 'Bob' } });
Expand Down
23 changes: 4 additions & 19 deletions packages/skill-tests/src/formio-form/wizards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,13 @@
// Behavior tests for plugin/skills/formio-form/references/wizards.md —
// wizard display mode, programmatic page navigation, conditional pages.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { Formio } from '@formio/js';
import { describe, expect, it, vi } from 'vitest';
import { conditionalWizardDefinition } from './fixtures/wizard-external';

const containers: HTMLElement[] = [];

function container(): HTMLElement {
const el = document.createElement('div');
document.body.appendChild(el);
containers.push(el);
return el;
}

afterEach(() => {
for (const el of containers.splice(0)) {
el.remove();
}
});
import { createForm } from './renderer-harness';

describe('wizards.md — page navigation', () => {
it('exposes programmatic next/previous page navigation', async () => {
const wizard = await Formio.createForm(container(), conditionalWizardDefinition);
const wizard = await createForm(conditionalWizardDefinition);
expect(wizard.pages.length).toBe(3);
expect(wizard.page).toBe(0);

Expand All @@ -37,7 +22,7 @@ describe('wizards.md — page navigation', () => {

describe('wizards.md — conditional pages', () => {
it('drops a page whose condition is false', async () => {
const wizard = await Formio.createForm(container(), conditionalWizardDefinition);
const wizard = await createForm(conditionalWizardDefinition);
expect(wizard.pages.length).toBe(3);

wizard.getComponent('wantsExtras').setValue('no');
Expand Down
Loading