@tft/crispr-forms-testing 21.0.12 → 21.0.14

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.
Files changed (2) hide show
  1. package/README.md +163 -0
  2. package/package.json +6 -1
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @tft/crispr-forms-testing
2
+
3
+ Playwright helpers and test factories for apps built with [`@tft/crispr-forms`](../crispr-forms/README.md).
4
+
5
+ Point it at the same `FormConfig` your component renders and it generates a standard set of e2e tests for every field, covering labels, hints, options and validation errors. When you need to write your own assertions, it also provides locators and actions that already handle Angular Material's quirks.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install --save-dev @tft/crispr-forms-testing
11
+ ```
12
+
13
+ Peer dependencies: `@playwright/test` (>= 1.40), `@angular/compiler` and `@angular/forms` (^21), and `@tft/crispr-forms`.
14
+
15
+ The package re-exports everything from `@playwright/test`, so a spec file needs only one import:
16
+
17
+ ```ts
18
+ import { test, expect, testForm, getInput } from '@tft/crispr-forms-testing';
19
+ ```
20
+
21
+ Importing the package also loads `@angular/compiler`. crispr-forms ships in partial-compilation (Ivy linker) format, and its configs need the JIT compiler to load in Node. If a Playwright file imports a crispr-forms config **without** importing this package, add `import '@angular/compiler';` at the top of it.
22
+
23
+ ## Quick start
24
+
25
+ Keep your form config in its own file so the component and the spec can share it:
26
+
27
+ ```ts
28
+ // profile.config.ts
29
+ export const profileFormConfig: FormConfig = {
30
+ fields: [
31
+ { controlType: ControlType.INPUT, controlName: 'name', label: 'Name', validators: [Validators.required] },
32
+ { controlType: ControlType.SELECT, controlName: 'role', label: 'Role', options: [/* ... */] },
33
+ ],
34
+ };
35
+ ```
36
+
37
+ Then generate the tests from it:
38
+
39
+ ```ts
40
+ // profile.spec.ts
41
+ import { test, testForm } from '@tft/crispr-forms-testing';
42
+ import { profileFormConfig } from '../app/profile/profile.config';
43
+
44
+ test.describe('Profile form', () => {
45
+ test.beforeEach(async ({ page }) => {
46
+ await page.goto('/profile');
47
+ });
48
+
49
+ testForm(profileFormConfig);
50
+ });
51
+ ```
52
+
53
+ Because the tests are derived from the config, a validator added there is tested automatically.
54
+
55
+ To cover just some fields, call `testField` per field instead:
56
+
57
+ ```ts
58
+ for (const field of profileFormConfig.fields) {
59
+ testField(field);
60
+ }
61
+ ```
62
+
63
+ ## Generated tests
64
+
65
+ `testForm(config)` calls `testField` for every top-level field. `testField(config)` opens a `test.describe` named after the `controlName` and picks the factory for its `controlType`. Each factory only registers tests for what the config defines; a field without a `hint`, for example, gets no hint test.
66
+
67
+ | Control type | Factory | Tests |
68
+ |---|---|---|
69
+ | `INPUT` | `testInputField` | label, input type, placeholder, hint, required, min, max, maxLength |
70
+ | `TEXTAREA` | `testTextareaField` | label, hint, required |
71
+ | `CHECKBOX` | `testCheckboxField` | label, `requiredTrue` error |
72
+ | `SELECT` | `testSelectField` | label, hint, option count, selecting an option, required |
73
+ | `DATEPICKER` | `testDatepickerField` | label, hint, toggle button, required |
74
+ | `SLIDER` | `testSliderField` | renders, label, min, max |
75
+ | `AUTOCOMPLETE` | `testAutocompleteField` | label, hint, dropdown, selecting an option, required |
76
+ | `AUTOCOMPLETE_CHIPLIST` | `testAutocompleteChiplistField` | label, hint, typing shows options, add chip, remove chip, `minArrayLength` error |
77
+ | `FILE_UPLOAD` | `testFileUploadField` | label, drop zone text, select button |
78
+ | `UNIT_CONVERSION` | `testUnitConversionField` | label, number input, unit select |
79
+
80
+ `testForm` and `testField` do **not** cover:
81
+
82
+ - Fields without a `controlName` (headings, dividers, buttons). These are skipped.
83
+ - `SUB_GROUP` and `GROUP_LIST` children. Nested fields aren't walked; test them with the helpers below.
84
+ - `IMAGE_UPLOAD` and `MAP`. These have no factory yet.
85
+
86
+ The individual factories are exported too, if you want one suite for one field.
87
+
88
+ ## Test ids
89
+
90
+ Every crispr-forms control renders a `data-testid` set to its control path. All helpers take this path as their `testId` argument.
91
+
92
+ | Field | `data-testid` |
93
+ |---|---|
94
+ | Top-level `firstName` | `firstName` |
95
+ | `firstName` in sub group `address` | `address.firstName` |
96
+ | `firstName` in group list `contacts`, item 2 | `contacts[2].firstName` |
97
+ | Nested in both | `contacts[2].address.city` |
98
+
99
+ Fields without a `controlName`, such as buttons, have no test id by default. Set `testId` on the field config to add one. Any parent prefix still applies, so `testId: 'save'` inside sub group `actions` renders `actions.save`.
100
+
101
+ ## Helpers
102
+
103
+ ### Locators
104
+
105
+ | Helper | Returns |
106
+ |---|---|
107
+ | `getMatFormField(page, testId)` | The element carrying the `data-testid`. Usually a `mat-form-field`, a `mat-checkbox` or a `mat-slider`. |
108
+ | `getInput(page, testId)` | The `input` inside the field |
109
+ | `getLabel(page, testId)` | The `mat-label` inside the field |
110
+ | `getHint(page, testId)` | The `mat-hint` inside the field |
111
+ | `getMatSelect(page, testId)` | The `mat-select` inside a select field |
112
+ | `getCheckbox(page, testId)` | The `mat-checkbox` element |
113
+ | `getAutocompleteInput(page, testId)` | The visible text input of an autocomplete or chiplist |
114
+ | `getError(page, testId, fieldType = 'input')` | The displayed error message. Pass the field's kebab-case type for non-input fields: `'select'`, `'textarea'`, `'checkbox'`, `'datepicker'`, `'autocomplete'` or `'autocomplete-chiplist'`. |
115
+
116
+ ### Actions
117
+
118
+ | Helper | Does |
119
+ |---|---|
120
+ | `touchInput(page, testId)` | Clicks the input, then Tabs away to blur it |
121
+ | `touchSelect(page, testId)` | Opens the select, presses Escape, then Tabs away |
122
+ | `touchCheckbox(page, testId)` | Clicks twice, so the box ends unchecked, then Tabs away |
123
+ | `touchAutocomplete(page, testId)` | Clicks the visible input, then Tabs away |
124
+ | `touchDatepicker(page, testId)` | Clicks the input, then Tabs away |
125
+ | `openSelect(page, testId)` | Opens the select panel |
126
+ | `selectOption(page, testId, optionLabel)` | Opens the select and picks the option with that label |
127
+ | `submitForm(page, nth = 0)` | Submits the `nth` crispr form on the page, which shows every control's errors |
128
+
129
+ ### Example
130
+
131
+ ```ts
132
+ import { test, expect, getInput, getError, touchInput, submitForm } from '@tft/crispr-forms-testing';
133
+
134
+ test('name is required', async ({ page }) => {
135
+ await page.goto('/profile');
136
+
137
+ await touchInput(page, 'name');
138
+ await expect(getError(page, 'name')).toHaveText('This field is required');
139
+
140
+ await getInput(page, 'name').fill('Ada');
141
+ await expect(getError(page, 'name')).toHaveCount(0);
142
+ });
143
+
144
+ test('nested list item shows its errors on submit', async ({ page }) => {
145
+ await page.goto('/profile');
146
+ await submitForm(page);
147
+ await expect(getError(page, 'contacts[0].email')).toBeVisible();
148
+ });
149
+ ```
150
+
151
+ ## Gotchas
152
+
153
+ **Showing errors on autocomplete and chiplist fields.** Their validators are on a hidden control, not on the visible input, so blurring the input never touches the validated control. Call `submitForm(page)` instead: it forces every control in the form to display its errors.
154
+
155
+ **Why `submitForm` doesn't click.** crispr-forms submits through a hidden `display: none` button, which Playwright can't click. `submitForm` clicks it from inside the page with `page.evaluate`.
156
+
157
+ **Select fields need a Tab after Escape.** Escape closes the panel but leaves the select focused. Errors appear on blur, so `touchSelect` also presses Tab.
158
+
159
+ **`getError` targets the message, not the host.** The `tft-control-error` element is always in the DOM, even with no error, and autocomplete/chiplist fields render two of them. `getError` matches the inner `p.tft-control-error`, which only exists while a message is shown. That keeps Playwright's strict mode happy, and `toHaveCount(0)` reliably asserts "no error".
160
+
161
+ **Slider labels and bounds.** Sliders aren't inside a `mat-form-field`, so their label is a plain `<label>` and `getLabel` won't find it; use `getMatFormField(page, id).locator('label')`. Read min and max from the native `min`/`max` attributes on `input[matSliderThumb]`, not from `aria-valuemin`/`aria-valuemax`.
162
+
163
+ **Unit conversion `required` can't be tested.** The visible input is a separate control from the one that holds the validators, so a required error never shows on the field.
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@tft/crispr-forms-testing",
3
- "version": "21.0.12",
3
+ "version": "21.0.14",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/nayfin/tft.git",
7
+ "directory": "libs/crispr-forms-testing"
8
+ },
4
9
  "main": "index.js",
5
10
  "types": "index.d.ts",
6
11
  "exports": {