@burojs/test-helpers 0.3.0 → 0.5.0

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/index.d.cts CHANGED
@@ -1,42 +1,89 @@
1
- import { ErasedQueryDescriptor, ErasedMutationDescriptor, BatchedQueryOperation, DataProvider } from '@burojs/core';
2
-
1
+ //#region src/types.d.ts
3
2
  /**
4
- * Subset of Playwright's Page API that we depend on.
5
- * Real consumers pass a real Page; tests pass a mock.
3
+ * The page surface the page objects compose testids on.
4
+ *
5
+ * **The locator type is a parameter, not a fixed shape.** Every factory in
6
+ * this package is written as `createX<L>(page: PlaywrightPage<L>, …): X<L>`,
7
+ * so `L` is INFERRED from whatever page is handed in: a real
8
+ * `@playwright/test` `Page` yields real `Locator`s — which is what makes
9
+ * `expect(view.row(id).cell('status')).toHaveText('paid')` type-check — while
10
+ * a hand-written double (see `tests/_mock-page.ts`) yields the double's own
11
+ * locator type and still exercises the string composition without a browser.
12
+ *
13
+ * It did not always work that way, and the reason is worth keeping.
14
+ * `PlaywrightPage` used to be a fixed structural subset whose members
15
+ * returned a fixed structural `PlaywrightLocator`. Under `strictFunctionTypes`
16
+ * a real `Page` was NOT assignable to it — its `getByRole` takes Playwright's
17
+ * `AriaRole` union, the subset declared the wider `string`, and a parameter
18
+ * may not widen — so `createBuroPage(realPage)` was a type error. Even had it
19
+ * been assignable, every locator came back as the structural type, which
20
+ * `expect()` does not accept, leaving a consumer with `isVisible()`-style
21
+ * boolean reads and no web-first (auto-waiting) assertion at all. Both defects
22
+ * were invisible for as long as the only caller was this package's own mock:
23
+ * the first real consumer (`examples/marketplace-ops/e2e`) hit them on its
24
+ * first line. Keep the parameter.
25
+ *
26
+ * Only the members the factories actually CALL are required. `getByRole`,
27
+ * `click`, `fill`, `textContent` and `isVisible` were declared here and used
28
+ * by nothing — the factories return locators and let the caller drive them —
29
+ * and `getByRole` in particular was the member that broke assignability, so
30
+ * requiring it bought nothing and cost everything.
6
31
  */
7
- interface PlaywrightLocator {
8
- click: () => Promise<void>;
9
- fill: (value: string) => Promise<void>;
10
- textContent: () => Promise<string | null>;
11
- getByTestId: (id: string) => PlaywrightLocator;
12
- getByRole: (role: string, opts?: {
13
- name?: string | RegExp;
14
- }) => PlaywrightLocator;
15
- isVisible: () => Promise<boolean>;
32
+ interface PlaywrightPage<TLocator = unknown> {
33
+ goto: (url: string) => Promise<unknown>;
34
+ getByTestId: (id: string) => TLocator;
35
+ /**
36
+ * Attribute/CSS selection. Needed for the SET-shaped accessors — "every
37
+ * rendered row", "this column across every row" — which `getByTestId`
38
+ * cannot express: those testids embed a record id, so they can only be
39
+ * addressed by prefix.
40
+ */
41
+ locator: (selector: string) => TLocator;
42
+ url: () => string;
16
43
  }
17
- interface PlaywrightPage {
18
- goto: (url: string) => Promise<unknown>;
19
- getByTestId: (id: string) => PlaywrightLocator;
20
- getByRole: (role: string, opts?: {
21
- name?: string | RegExp;
22
- }) => PlaywrightLocator;
23
- url: () => string;
44
+ /**
45
+ * The one capability the page objects need from a locator THEMSELVES rather
46
+ * than handing it to the caller: {@link RowObject.open} clicks the row it
47
+ * addresses. Everything else in this package returns a locator and stops, so
48
+ * this is the whole constraint the locator parameter carries.
49
+ */
50
+ interface ClickableLocator {
51
+ click: () => Promise<unknown>;
24
52
  }
25
-
26
- interface FormFieldObject {
27
- input: () => PlaywrightLocator;
28
- error: () => PlaywrightLocator;
29
- help: () => PlaywrightLocator;
53
+ /**
54
+ * A convenience shape for writing a page DOUBLE (`PlaywrightPage<PlaywrightLocator>`).
55
+ * Nothing in this package requires it — a real consumer passes a real `Page`
56
+ * and gets real `Locator`s back.
57
+ */
58
+ interface PlaywrightLocator {
59
+ click: () => Promise<void>;
60
+ fill: (value: string) => Promise<void>;
61
+ textContent: () => Promise<string | null>;
62
+ getByTestId: (id: string) => PlaywrightLocator;
63
+ isVisible: () => Promise<boolean>;
64
+ }
65
+ //#endregion
66
+ //#region src/form-view.d.ts
67
+ interface FormFieldObject<L> {
68
+ input: () => L;
69
+ error: () => L;
70
+ help: () => L;
30
71
  }
31
- interface FormView {
32
- field: (fieldName: string) => FormFieldObject;
33
- section: (sectionKey: string) => PlaywrightLocator;
34
- submit: () => PlaywrightLocator;
35
- cancel: () => PlaywrightLocator;
36
- action: (actionName: string) => PlaywrightLocator;
72
+ interface FormView<L extends ClickableLocator> {
73
+ field: (fieldName: string) => FormFieldObject<L>;
74
+ submit: () => L;
75
+ cancel: () => L;
76
+ action: (actionName: string) => L;
77
+ /**
78
+ * A keyed node of the record screen (a panel, tabs, a step) by its key,
79
+ * inside `[data-buro-region="<region>"]` (default the body). Nodes carry no
80
+ * test id of their own: the key and the region locate them.
81
+ */
82
+ node: (key: string, region?: 'record-header' | 'record-body') => L;
37
83
  }
38
- declare function createFormView(page: PlaywrightPage, resource: string, view: 'create' | 'edit' | 'show'): FormView;
39
-
84
+ declare function createFormView<L extends ClickableLocator>(page: PlaywrightPage<L>, resource: string, view: 'create' | 'edit' | 'show'): FormView<L>;
85
+ //#endregion
86
+ //#region src/list-view.d.ts
40
87
  /**
41
88
  * A row's address inside the filter-builder draft: `[]` is the root group,
42
89
  * `[0]` is its first row, `[1, 0]` is the first row of the group nested at
@@ -47,155 +94,348 @@ declare function createFormView(page: PlaywrightPage, resource: string, view: 'c
47
94
  * `list-filter:<r>:group::add-row`).
48
95
  */
49
96
  type FilterRowPath = readonly number[];
50
- interface ListView {
51
- goto: () => Promise<void>;
52
- row: (recordId: string) => RowObject;
53
- search: () => PlaywrightLocator;
54
- /** The filter region container — `list-filters:<resource>`. */
55
- filtersContainer: () => PlaywrightLocator;
56
- /** Opens/closes the filter-builder popover. */
57
- filterButton: () => PlaywrightLocator;
58
- /** The popover body once open. */
59
- filterPanel: () => PlaywrightLocator;
60
- /** "+ Filter" — appends a new predicate row to the group at `groupPath` (root: `[]`). */
61
- filterAddRow: (groupPath?: FilterRowPath) => PlaywrightLocator;
62
- /** Field-select trigger for the row at `path`. */
63
- filterRowField: (path: FilterRowPath) => PlaywrightLocator;
64
- /** One field option inside an open field-select. */
65
- filterRowFieldOption: (path: FilterRowPath, field: string) => PlaywrightLocator;
66
- /** Operator-select trigger for the row at `path`. */
67
- filterRowOp: (path: FilterRowPath) => PlaywrightLocator;
68
- /** One operator option inside an open operator-select. */
69
- filterRowOpOption: (path: FilterRowPath, op: string) => PlaywrightLocator;
70
- /** Value control trigger/input for the row at `path` — shape depends on the field kind (text box, enum/relation/boolean popover). */
71
- filterRowValue: (path: FilterRowPath) => PlaywrightLocator;
72
- /** Removes the row at `path` from the draft (inside the panel, before Apply). */
73
- filterRowRemove: (path: FilterRowPath) => PlaywrightLocator;
74
- /** Commits the draft — applies it and closes the panel. */
75
- filterApply: () => PlaywrightLocator;
76
- /** Clears the draft back to empty (still requires Apply to take effect). */
77
- filterReset: () => PlaywrightLocator;
78
- /** One applied-filter badge outside the panel, by its top-level index. */
79
- filterBadge: (index: number) => PlaywrightLocator;
80
- /** A badge's × — removes that row immediately, no Apply needed. */
81
- filterBadgeRemove: (index: number) => PlaywrightLocator;
82
- /** "Clear all" — empties both the pinned slot and the builder group. */
83
- filterClearAll: () => PlaywrightLocator;
84
- /** An always-visible pinned control (`views.list.filters.pinned`) for `field`, rendered outside the panel. */
85
- filterPinned: (field: string) => PlaywrightLocator;
86
- paginationPrev: () => PlaywrightLocator;
87
- paginationNext: () => PlaywrightLocator;
88
- empty: () => PlaywrightLocator;
89
- toolbar: () => PlaywrightLocator;
90
- table: () => PlaywrightLocator;
97
+ interface ListView<L extends ClickableLocator> {
98
+ goto: () => Promise<void>;
99
+ row: (recordId: string) => RowObject<L>;
100
+ /**
101
+ * EVERY rendered row of this resource, as one locator — the set, not a
102
+ * named member of it. `row(recordId)` cannot express it: a row's testid
103
+ * embeds its record id, so the set is only addressable by prefix.
104
+ *
105
+ * This is the accessor a list assertion is actually built on
106
+ * (`await expect(list.rows()).toHaveCount(n)`), and it was missing for as
107
+ * long as the only consumer of this package was a mock that never counted
108
+ * anything.
109
+ */
110
+ rows: () => L;
111
+ /**
112
+ * One column across every rendered row, in row order — again a set, again
113
+ * prefix-addressed. Reading a whole column through ONE locator matters for
114
+ * more than convenience: a per-row loop awaits between reads and can meet a
115
+ * detached node mid-re-render, which turns a polling wait into a thrown
116
+ * error instead of another attempt.
117
+ */
118
+ column: (fieldName: string) => L;
119
+ /** The column header's menu trigger (sort / pin / hide / move). */
120
+ columnMenu: (fieldName: string) => L;
121
+ /** The sort item inside an OPEN column header menu. */
122
+ columnSort: (fieldName: string, direction: 'asc' | 'desc') => L;
123
+ /**
124
+ * The reorder item inside an OPEN column header menu — moves the column one
125
+ * position left or right.
126
+ *
127
+ * ⚠️ What it moves is **column layout**, which is a different persistence
128
+ * mechanism from everything else on this object: order, width and pinning
129
+ * live in raw `localStorage` under `buro:table-layout:<resource>:{order,
130
+ * sizing,pinning}` and are carried by neither the URL nor a saved view. See
131
+ * {@link ListView.savedViews}.
132
+ */
133
+ columnMove: (fieldName: string, direction: 'left' | 'right') => L;
134
+ /** The Columns menu trigger — show/hide columns, reset widths, restore defaults. */
135
+ columnsToggle: () => L;
136
+ /**
137
+ * One column's row inside the OPEN Columns menu. A column the resource
138
+ * declares in `views.list.columns` is REQUIRED unless
139
+ * `views.list.requiredColumns` says otherwise, and renders `disabled` with
140
+ * `data-required="true"` — only `optionalColumns` are actually clickable.
141
+ */
142
+ columnsOption: (columnId: string) => L;
143
+ /** The rows-per-page select trigger; its text is the active page size. */
144
+ pageSize: () => L;
145
+ search: () => L;
146
+ /** The filter region container — `list-filters:<resource>`. */
147
+ filtersContainer: () => L;
148
+ /** Opens/closes the filter-builder popover. */
149
+ filterButton: () => L;
150
+ /** The popover body once open. */
151
+ filterPanel: () => L;
152
+ /** "+ Filter" — appends a new predicate row to the group at `groupPath` (root: `[]`). */
153
+ filterAddRow: (groupPath?: FilterRowPath) => L;
154
+ /** Field-select trigger for the row at `path`. */
155
+ filterRowField: (path: FilterRowPath) => L;
156
+ /** One field option inside an open field-select. */
157
+ filterRowFieldOption: (path: FilterRowPath, field: string) => L;
158
+ /** Operator-select trigger for the row at `path`. */
159
+ filterRowOp: (path: FilterRowPath) => L;
160
+ /** One operator option inside an open operator-select. */
161
+ filterRowOpOption: (path: FilterRowPath, op: string) => L;
162
+ /** Value control trigger/input for the row at `path` — shape depends on the field kind (text box, enum/relation/boolean popover). */
163
+ filterRowValue: (path: FilterRowPath) => L;
164
+ /** Removes the row at `path` from the draft (inside the panel, before Apply). */
165
+ filterRowRemove: (path: FilterRowPath) => L;
166
+ /** Commits the draft — applies it and closes the panel. */
167
+ filterApply: () => L;
168
+ /** Clears the draft back to empty (still requires Apply to take effect). */
169
+ filterReset: () => L;
170
+ /** One applied-filter badge outside the panel, by its top-level index. */
171
+ filterBadge: (index: number) => L;
172
+ /** A badge's × — removes that row immediately, no Apply needed. */
173
+ filterBadgeRemove: (index: number) => L;
174
+ /** "Clear all" — empties both the pinned slot and the builder group. */
175
+ filterClearAll: () => L;
176
+ /** An always-visible pinned control (`views.list.filters.pinned`) for `field`, rendered outside the panel. */
177
+ filterPinned: (field: string) => L;
178
+ /**
179
+ * The floating bulk bar, which appears as soon as ANY row is selected —
180
+ * even for a resource that declares no bulk action, because it also carries
181
+ * the built-in Export entry and the selection count.
182
+ */
183
+ bulkBar: () => L;
184
+ /** One declared bulk action's button inside the bar (`actions.bulk`). */
185
+ bulkAction: (actionName: string) => L;
186
+ paginationPrev: () => L;
187
+ paginationNext: () => L;
188
+ /**
189
+ * The load-more button a CURSOR-paginated list renders instead of the
190
+ * numbered pager (`views.list.pagination: 'cursor'` → `<CursorLoadMore>`).
191
+ *
192
+ * ⚠️ NOT resource-scoped, because the control itself is not: `CursorLoadMore`
193
+ * carries a bare `data-testid="cursor-load-more"` with no resource segment,
194
+ * unlike every other id above. A page rendering two cursor lists at once
195
+ * would need `.first()`/`.nth()` to tell them apart. Recorded rather than
196
+ * papered over — inventing a scoped id here would only make this object
197
+ * disagree with the component.
198
+ *
199
+ * ⚠️ `<CursorLoadMore>` renders NOTHING when there is no next page, so the
200
+ * end of a cursor list is "this locator resolves to no element", not a
201
+ * disabled button. That is what makes it usable as an end-of-list assertion.
202
+ *
203
+ * Like `columnMenu`/`columnSort`, this id has no `useTestIds` factory behind
204
+ * it, so `page-object-vocabulary` tests have nothing to check it against
205
+ * (the pre-existing F6 gap — see the saved-views block below for what that
206
+ * costs, measured).
207
+ */
208
+ loadMore: () => L;
209
+ empty: () => L;
210
+ toolbar: () => L;
211
+ table: () => L;
212
+ /** The Views dropdown trigger in the list toolbar. */
213
+ savedViews: () => L;
214
+ /** The dropdown body once open. */
215
+ savedViewsMenu: () => L;
216
+ /**
217
+ * EVERY view entry in the open dropdown (adopter presets and user-saved
218
+ * alike), as one locator — the set, not a named member of it, because a
219
+ * user view's id is a uuid minted at save time and no test can know it in
220
+ * advance. Prefix-addressed for the same reason {@link ListView.rows} is.
221
+ */
222
+ savedViewEntries: () => L;
223
+ /** One view entry by id — for an adopter preset declared in `views.list.savedViews`, whose id IS known. */
224
+ savedView: (viewId: string) => L;
225
+ /** "Save current view…" — opens the save dialog. */
226
+ savedViewSave: () => L;
227
+ /** The label input inside the open save dialog. */
228
+ savedViewSaveInput: () => L;
229
+ /** The save dialog's confirm button; disabled while the label is empty. */
230
+ savedViewSaveConfirm: () => L;
231
+ /**
232
+ * "Manage views…" — opens the manage dialog. Rendered only when at least
233
+ * one entry exists, so `toHaveCount(0)` is a usable "this browser has no
234
+ * saved views" assertion.
235
+ */
236
+ savedViewManage: () => L;
237
+ /** One entry's Delete button inside the open manage dialog. */
238
+ savedViewManageDelete: (viewId: string) => L;
91
239
  }
92
- interface RowObject {
93
- cell: (fieldName: string) => PlaywrightLocator;
94
- action: (actionName: string) => PlaywrightLocator;
95
- checkbox: () => PlaywrightLocator;
96
- open: () => Promise<void>;
240
+ interface RowObject<L extends ClickableLocator> {
241
+ /**
242
+ * The row element itself. Without it a caller cannot say "this row is on
243
+ * screen" or "this row is gone" — the two assertions a list test needs
244
+ * most, and the ones `cell()`/`action()` cannot stand in for (a row can be
245
+ * present while a given cell or action is not).
246
+ */
247
+ root: () => L;
248
+ cell: (fieldName: string) => L;
249
+ action: (actionName: string) => L;
250
+ checkbox: () => L;
251
+ open: () => Promise<void>;
97
252
  }
98
253
  interface ListViewOptions {
99
- basePath?: string;
254
+ /**
255
+ * What {@link ListView.goto} navigates to. Defaults to `/<resource>`, which
256
+ * assumes the route is named after the resource — often false (a `order`
257
+ * resource routed at `/orders`), and it carries no query string, so an app
258
+ * whose test isolation travels on the URL has to pass its whole launch URL
259
+ * here.
260
+ */
261
+ basePath?: string;
100
262
  }
101
- declare function createListView(page: PlaywrightPage, resource: string, options?: ListViewOptions): ListView;
102
-
103
- interface PickerObject {
104
- dialog: () => PlaywrightLocator;
105
- popover: () => PlaywrightLocator;
106
- option: (value: string) => PlaywrightLocator;
107
- search: () => PlaywrightLocator;
108
- clear: () => PlaywrightLocator;
109
- quickCreate: () => PlaywrightLocator;
263
+ declare function createListView<L extends ClickableLocator>(page: PlaywrightPage<L>, resource: string, options?: ListViewOptions): ListView<L>;
264
+ //#endregion
265
+ //#region src/picker-object.d.ts
266
+ /**
267
+ * The relation picker's addresses, as a test drives them.
268
+ *
269
+ * ⚠️ **Every id here was wrong until SP9 task 5, and nothing could see it.**
270
+ * This object used to ask for `picker-dialog:` / `picker-popover:` /
271
+ * `picker-search:` / `picker-option:` / `picker-clear:` /
272
+ * `picker-quickcreate:`, and `@burojs/react`'s `useTestIds()` composed exactly
273
+ * the same six — while `@burojs/ui-shadcn`'s `<RelationPicker>` has always
274
+ * emitted `relation-picker:<target>:…`. Neither vocabulary had a single
275
+ * producer. The cross-check that exists to prevent this
276
+ * (`examples/marketplace-ops/tests/page-object-vocabulary.test.tsx`) was green
277
+ * throughout, because it compared these two copies with each other and neither
278
+ * of them with the component. The producer side is pinned now, in
279
+ * `packages/ui-shadcn/tests/relation-picker-testids.test.tsx`.
280
+ *
281
+ * `quickCreate` is not re-spelled but REMOVED: quick-create inside the picker
282
+ * is deferred to the picker's v2 and there is no element to address. `popover`
283
+ * is likewise gone — the popover content carries no id; {@link PickerObject.list}
284
+ * is the element inside it that does.
285
+ *
286
+ * **`trigger()` is the standalone spelling.** Inside a record form the
287
+ * form overrides the trigger's testid with its own
288
+ * `form-field-input:<resource>:<view>:<field>`, so a form field's picker is
289
+ * opened through `resource(r).create().field(f).input()` and only its popover
290
+ * children are addressed through this object.
291
+ */
292
+ interface PickerObject<L> {
293
+ /** The standalone trigger button. In a form, use the form field's `input()`. */
294
+ trigger: () => L;
295
+ /** The popover's search box. */
296
+ search: () => L;
297
+ /** The popover's result list. */
298
+ list: () => L;
299
+ /** One result row, by the target record's id. */
300
+ option: (value: string) => L;
301
+ /** The "No results." row. */
302
+ empty: () => L;
303
+ /** The Clear link — present only when the picker is clearable and set. */
304
+ clear: () => L;
305
+ /** The "Advanced search" link that escalates to {@link PickerObject.dialog}. */
306
+ advanced: () => L;
307
+ /** The "Open list" link to the target's own list page. */
308
+ viewAll: () => L;
309
+ /** The escalation modal. */
310
+ dialog: () => L;
311
+ /** The escalation modal's own search box. */
312
+ dialogSearch: () => L;
313
+ /** The disabled trigger shown when the target resource is unregistered. */
314
+ unregistered: () => L;
110
315
  }
111
- declare function createPickerObject(page: PlaywrightPage, targetResource: string): PickerObject;
112
-
113
- type ShowView = FormView;
114
- declare function createShowView(page: PlaywrightPage, resource: string): ShowView;
115
-
116
- interface ResourceObject {
117
- name: string;
118
- list: (options?: ListViewOptions) => ListView;
119
- edit: (recordId: string) => FormView;
120
- show: (recordId: string) => ShowView;
121
- create: () => FormView;
122
- picker: () => PickerObject;
316
+ declare function createPickerObject<L>(page: PlaywrightPage<L>, targetResource: string): PickerObject<L>;
317
+ //#endregion
318
+ //#region src/show-view.d.ts
319
+ interface ShowView<L extends ClickableLocator> extends FormView<L> {
320
+ /**
321
+ * The show page root — `useTestIds`' `pageShow(resource, recordId)`.
322
+ *
323
+ * The ONE show accessor that uses the record id. Every other locator on this
324
+ * view comes from `createFormView`, whose testids carry no record id at all
325
+ * (see {@link import('./resource-object.js').ResourceObject.show}), so this
326
+ * is what lets a test assert it is looking at the show page OF A GIVEN
327
+ * RECORD rather than at whatever detail page happens to be mounted.
328
+ */
329
+ root: () => L;
123
330
  }
124
- declare function createResourceObject(page: PlaywrightPage, name: string): ResourceObject;
125
-
126
- interface BuroPage {
127
- resource: (name: string) => ResourceObject;
128
- contextPicker: (sliceName: string) => PlaywrightLocator;
129
- notification: (type: 'success' | 'error' | 'progress' | 'info') => PlaywrightLocator;
130
- actionConfirm: (resource: string, scope: string, actionName: string) => PlaywrightLocator;
131
- page: PlaywrightPage;
331
+ declare function createShowView<L extends ClickableLocator>(page: PlaywrightPage<L>, resource: string, recordId: string): ShowView<L>;
332
+ //#endregion
333
+ //#region src/resource-object.d.ts
334
+ interface ResourceObject<L extends ClickableLocator> {
335
+ name: string;
336
+ list: (options?: ListViewOptions) => ListView<L>;
337
+ /**
338
+ * ⚠️ `recordId` is accepted and IGNORED: the form testids
339
+ * (`form-field:<resource>:edit:<field>`) carry no record id, so an edit view
340
+ * addressed for record A and one addressed for record B are the same
341
+ * locators. Only one edit form is ever mounted at a time, so this is
342
+ * harmless in practice — but the parameter reads like a scope and is not
343
+ * one.
344
+ */
345
+ edit: (recordId: string) => FormView<L>;
346
+ /**
347
+ * ⚠️ `recordId` reaches {@link ShowView.root} and NOTHING ELSE. The show
348
+ * view's field/section/action locators all come from `createFormView`, whose
349
+ * testids carry no record id, so they are the same locators for record A and
350
+ * record B. `root()` is the one accessor that is genuinely scoped to the
351
+ * record — assert on it first if a test needs to know WHICH record's page is
352
+ * mounted.
353
+ */
354
+ show: (recordId: string) => ShowView<L>;
355
+ create: () => FormView<L>;
356
+ picker: () => PickerObject<L>;
132
357
  }
133
- declare function createBuroPage(page: PlaywrightPage): BuroPage;
134
-
135
- interface ConfirmDialogObject {
136
- dialog: () => PlaywrightLocator;
137
- yes: () => PlaywrightLocator;
138
- no: () => PlaywrightLocator;
358
+ declare function createResourceObject<L extends ClickableLocator>(page: PlaywrightPage<L>, name: string): ResourceObject<L>;
359
+ //#endregion
360
+ //#region src/buro-page.d.ts
361
+ interface BuroPage<L extends ClickableLocator> {
362
+ resource: (name: string) => ResourceObject<L>;
363
+ /**
364
+ * The sidebar entry for a registered resource — `<NavMain>`'s
365
+ * `tid.navMenuItem(resource)`. The whole navigation half of a suite used to
366
+ * spell this string by hand even though the factory existed (SP3 stage C
367
+ * final review, Important 2).
368
+ */
369
+ navMenuItem: (resource: string) => L;
370
+ /** The sidebar entry for an adopter-declared custom page, keyed by page key
371
+ * rather than resource name (`tid.sidebarCustomPage`). */
372
+ sidebarCustomPage: (pageKey: string) => L;
373
+ contextPicker: (sliceName: string) => L;
374
+ /** One option inside an open context picker — the half that actually
375
+ * performs a switch. Without it a caller can open the picker and not choose. */
376
+ contextPickerOption: (sliceName: string, value: string) => L;
377
+ /**
378
+ * The header language switcher (`<LanguageSwitch>`) and one locale button
379
+ * inside it. The trigger carries `data-language-state="<code>"`, which is
380
+ * how a test reads the ACTIVE locale without opening anything.
381
+ */
382
+ languageSwitch: () => L;
383
+ languageSwitchOption: (localeCode: string) => L;
384
+ /** `<LoginForm>`'s three controls, in the order a sign-in uses them. */
385
+ login: () => LoginFormObject<L>;
386
+ /**
387
+ * `<TaskRunPanel>`'s three addressable parts, keyed by the RUN's name — a
388
+ * task run has no resource to key on (a long server-side operation has no
389
+ * record, a bulk run spans a selection).
390
+ *
391
+ * `root()` carries `data-state`, which is how a spec reads the outcome
392
+ * without matching translated text.
393
+ */
394
+ taskRun: (name: string) => TaskRunObject<L>;
395
+ notification: (type: 'success' | 'error' | 'progress' | 'info') => L;
396
+ /**
397
+ * A flow's confirm dialog on a toolbar surface (`form-action:<resource>:<scope>:<action>`): the
398
+ * dialog a flow with `confirm` (or a fired warning) asks through before it writes (P4). Its
399
+ * buttons are the same id with `:proceed` / `:cancel` in place of `:dialog`. A row's flow asks
400
+ * through `list-row-action:<resource>:<id>:<action>:confirm:dialog`.
401
+ */
402
+ actionConfirm: (resource: string, scope: string, actionName: string) => L;
403
+ page: PlaywrightPage<L>;
139
404
  }
140
- declare function createConfirmDialog(page: PlaywrightPage, resource: string, scope: string, actionName: string): ConfirmDialogObject;
141
-
142
- type QueryKind = ErasedQueryDescriptor['kind'];
143
- type MutationKind = ErasedMutationDescriptor['kind'];
144
- interface ProviderObservedCall {
145
- readonly lifecycle: 'query' | 'mutation' | 'batch' | 'live-open' | 'live-subscribe';
146
- readonly kind?: QueryKind | MutationKind;
147
- readonly descriptor?: ErasedQueryDescriptor | ErasedMutationDescriptor;
148
- readonly descriptors?: readonly ErasedQueryDescriptor[];
149
- readonly signal?: AbortSignal;
405
+ interface TaskRunObject<L> {
406
+ root: () => L;
407
+ /** The bar; its `aria-valuenow`/`aria-valuemax` are the counts. */
408
+ progress: () => L;
409
+ /** One named unit that did not succeed — a refused row, or the step a run stopped at. */
410
+ failure: (subject: string) => L;
150
411
  }
151
- /** Specimens supplied by an adapter author; unsupported optional methods are omitted. */
152
- interface ProviderContractDescriptors {
153
- readonly queries?: Partial<Record<QueryKind, ErasedQueryDescriptor>>;
154
- readonly mutations?: Partial<Record<MutationKind, ErasedMutationDescriptor>>;
155
- readonly batch?: readonly BatchedQueryOperation[];
156
- /** Independent batch groups prove that grouping is explicit, never merged by the helper. */
157
- readonly batchGroups?: readonly (readonly BatchedQueryOperation[])[];
158
- /** List specimens for both pagination protocols, including expected totals/pageInfo. */
159
- readonly pagination?: {
160
- readonly offset?: ErasedQueryDescriptor;
161
- readonly cursor?: ErasedQueryDescriptor;
162
- };
163
- readonly live?: ErasedQueryDescriptor;
164
- /** Error specimens must make the selected handler reject with a DataError. */
165
- readonly errors?: readonly ProviderContractErrorSpecimen[];
166
- /** Hand-authored mapped results; assertions never derive expectations from an adapter. */
167
- readonly expected?: {
168
- readonly queries?: Partial<Record<QueryKind, unknown>>;
169
- readonly mutations?: Partial<Record<MutationKind, unknown>>;
170
- readonly batch?: unknown;
171
- readonly pagination?: {
172
- readonly offset?: unknown;
173
- readonly cursor?: unknown;
174
- };
175
- };
412
+ interface LoginFormObject<L> {
413
+ email: () => L;
414
+ password: () => L;
415
+ submit: () => L;
176
416
  }
177
- type ProviderContractErrorSpecimen = {
178
- readonly lifecycle: 'query';
179
- readonly kind: QueryKind;
180
- readonly descriptor: ErasedQueryDescriptor;
181
- } | {
182
- readonly lifecycle: 'mutation';
183
- readonly kind: MutationKind;
184
- readonly descriptor: ErasedMutationDescriptor;
185
- };
186
- interface ProviderContractFixture {
187
- create(): DataProvider | Promise<DataProvider>;
188
- descriptors: ProviderContractDescriptors;
189
- observeCalls(): readonly ProviderObservedCall[];
417
+ declare function createBuroPage<L extends ClickableLocator>(page: PlaywrightPage<L>): BuroPage<L>;
418
+ //#endregion
419
+ //#region src/confirm-dialog.d.ts
420
+ interface ConfirmDialogObject<L> {
421
+ dialog: () => L;
422
+ yes: () => L;
423
+ no: () => L;
190
424
  }
425
+ declare function createConfirmDialog<L>(page: PlaywrightPage<L>, resource: string, scope: string, actionName: string): ConfirmDialogObject<L>;
426
+ //#endregion
427
+ //#region src/index.d.ts
191
428
  /**
192
- * Registers transport-agnostic provider conformance tests. It intentionally
193
- * imports only core contracts: adapters supply their own deterministic client
194
- * and expose provider-boundary observations through the fixture.
429
+ * `@burojs/test-helpers` — the Playwright entry point.
430
+ *
431
+ * Everything exported here is loadable inside a `@playwright/test` process:
432
+ * page objects over the testid convention `useTestIds()` (`@burojs/react`)
433
+ * emits, and nothing else. The vitest-based provider contract moved to
434
+ * `@burojs/test-helpers/provider-contract` — see that module for the measured
435
+ * reason it can no longer share an entry point with these.
195
436
  */
196
- declare function dataProviderContract(name: string, fixture: ProviderContractFixture): void;
197
-
198
437
  declare const PACKAGE_NAME: "@burojs/test-helpers";
199
438
  declare const PACKAGE_VERSION: "0.0.0";
200
-
201
- export { type BuroPage, type ConfirmDialogObject, type FormFieldObject, type FormView, type ListView, type ListViewOptions, PACKAGE_NAME, PACKAGE_VERSION, type PickerObject, type PlaywrightLocator, type PlaywrightPage, type ProviderContractDescriptors, type ProviderContractFixture, type ProviderObservedCall, type ResourceObject, type RowObject, type ShowView, createBuroPage, createConfirmDialog, createFormView, createListView, createPickerObject, createResourceObject, createShowView, dataProviderContract };
439
+ //#endregion
440
+ export { type BuroPage, type ClickableLocator, type ConfirmDialogObject, type FilterRowPath, type FormFieldObject, type FormView, type ListView, type ListViewOptions, type LoginFormObject, PACKAGE_NAME, PACKAGE_VERSION, type PickerObject, type PlaywrightLocator, type PlaywrightPage, type ResourceObject, type RowObject, type ShowView, type TaskRunObject, createBuroPage, createConfirmDialog, createFormView, createListView, createPickerObject, createResourceObject, createShowView };
441
+ //# sourceMappingURL=index.d.cts.map