@truenas/ui-components 0.7.5 → 0.7.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truenas/ui-components",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"
@@ -1,17 +1,21 @@
1
1
  import type { Config } from 'jest';
2
2
 
3
3
  /**
4
- * `yarn test:scripts` — every build script under this directory that has tests.
4
+ * `yarn test:scripts` — the repo's node-environment tests: every build script under this
5
+ * directory that has tests, plus `release-config`, which tests repo-root configuration
6
+ * rather than a script and lives here because this is where a node-environment Jest
7
+ * project already runs.
5
8
  *
6
- * One Jest project per script package rather than one config rooted here, because
7
- * `<rootDir>` inside a project config resolves to that project's own directory: each
8
- * package keeps pointing ts-jest at its own `tsconfig.json`, and adding the next one
9
- * costs a line here rather than a merge of compiler options.
9
+ * One Jest project per package rather than one config rooted here, because `<rootDir>`
10
+ * inside a project config resolves to that project's own directory: each package keeps
11
+ * pointing ts-jest at its own `tsconfig.json`, and adding the next one costs a line here
12
+ * rather than a merge of compiler options.
10
13
  */
11
14
  const config: Config = {
12
15
  projects: [
13
16
  '<rootDir>/harness-docs/jest.config.ts',
14
17
  '<rootDir>/icon-sprite/jest.config.ts',
18
+ '<rootDir>/release-config/jest.config.ts',
15
19
  ],
16
20
  };
17
21
 
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The PR title check and the release step both decide what a commit header means, from
3
+ * two patterns written in two files, and nothing made them agree. A scope naming two
4
+ * components with a comma — `feat(autocomplete,chip-input): …` — passed the title check,
5
+ * merged, and then matched no header pattern at all, so the commit analyzer read it as
6
+ * having no type: it triggered no release and appeared in no changelog (#315).
7
+ *
8
+ * These tests read both files rather than restating either pattern, so drift in either
9
+ * one fails here instead of silently dropping a merged commit from the next release.
10
+ *
11
+ * They live under the library's script tests because `yarn test:scripts` is where this
12
+ * repo runs Jest in a node environment; what they cover is repo-root release
13
+ * configuration, which is why they reach four directories up.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+
18
+ const repoRoot = join(__dirname, '..', '..', '..', '..');
19
+
20
+ interface ParserOpts {
21
+ headerPattern: string;
22
+ headerCorrespondence: string[];
23
+ breakingHeaderPattern: string;
24
+ }
25
+
26
+ type PluginEntry = string | [string, { parserOpts?: ParserOpts }];
27
+
28
+ const releaseConfig = JSON.parse(
29
+ readFileSync(join(repoRoot, '.releaserc.json'), 'utf8')
30
+ ) as { plugins: PluginEntry[] };
31
+
32
+ /** Every semantic-release plugin that parses commit headers, with its plugin name. */
33
+ const parsingPlugins: [string, ParserOpts][] = [];
34
+
35
+ for (const plugin of releaseConfig.plugins) {
36
+ if (Array.isArray(plugin)) {
37
+ const [name, options] = plugin;
38
+
39
+ if (options.parserOpts) {
40
+ parsingPlugins.push([name, options.parserOpts]);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * The Bash pattern the PR title check runs, lifted from the workflow that runs it.
47
+ *
48
+ * `[[ =~ ]]` and `RegExp` both leave the end unanchored and the pattern anchors its own
49
+ * start, so a JS `RegExp` accepts what the check accepts for the constructs it uses:
50
+ * groups, alternation, character classes and `?`.
51
+ */
52
+ function readPrTitlePattern(): RegExp {
53
+ const workflow = readFileSync(join(repoRoot, '.github/workflows/pr-title.yml'), 'utf8');
54
+ const assignment = /^\s*pattern='([^']+)'\s*$/m.exec(workflow);
55
+
56
+ if (!assignment) {
57
+ throw new Error('no `pattern=<...>` assignment found in .github/workflows/pr-title.yml');
58
+ }
59
+
60
+ return new RegExp(assignment[1]);
61
+ }
62
+
63
+ const prTitleCheck = readPrTitlePattern();
64
+
65
+ /** What the release step reads out of a header, or null when it matches nothing. */
66
+ function parseHeader(opts: ParserOpts, header: string): Record<string, string | undefined> | null {
67
+ const match = new RegExp(opts.headerPattern).exec(header);
68
+
69
+ if (!match) {
70
+ return null;
71
+ }
72
+
73
+ return Object.fromEntries(
74
+ opts.headerCorrespondence.map((field, index) => [field, match[index + 1]])
75
+ );
76
+ }
77
+
78
+ const commaScopeHeader =
79
+ 'NAS-142381 / 27.0.0-BETA.1 / feat(autocomplete,chip-input): drive options from an async [dataSource] (#312)';
80
+
81
+ /**
82
+ * Headers the PR title check accepts. Every one is asserted to pass the check as well as
83
+ * to parse, so a case that stops being realistic fails rather than passing vacuously.
84
+ */
85
+ const acceptedHeaders: { name: string; header: string; type: string; scope?: string }[] = [
86
+ {
87
+ name: 'a single-word scope',
88
+ header: 'feat(table): let a selection survive a dataSource change',
89
+ type: 'feat',
90
+ scope: 'table',
91
+ },
92
+ {
93
+ name: 'the ticket and version prefix a squash commit carries',
94
+ header: 'NAS-143108 / 27.0.0-BETA.1 / fix(table): stop the pager aliasing pagination',
95
+ type: 'fix',
96
+ scope: 'table',
97
+ },
98
+ {
99
+ name: 'no scope at all',
100
+ header: 'chore: bump the toolchain',
101
+ type: 'chore',
102
+ scope: undefined,
103
+ },
104
+ {
105
+ name: 'two components separated by a comma (#315)',
106
+ header: commaScopeHeader,
107
+ type: 'feat',
108
+ scope: 'autocomplete,chip-input',
109
+ },
110
+ {
111
+ name: 'a comma and a space between components',
112
+ header: 'feat(autocomplete, chip-input): drive options from an async [dataSource]',
113
+ type: 'feat',
114
+ scope: 'autocomplete, chip-input',
115
+ },
116
+ {
117
+ name: 'a scope with a slash',
118
+ header: 'refactor(a11y/labels): fold the fallback into one helper',
119
+ type: 'refactor',
120
+ scope: 'a11y/labels',
121
+ },
122
+ ];
123
+
124
+ describe('.releaserc.json', () => {
125
+ it('has more than one plugin parsing commit headers, one of them the commit analyzer', () => {
126
+ expect(parsingPlugins.length).toBeGreaterThan(1);
127
+ expect(parsingPlugins.map(([name]) => name)).toContain('@semantic-release/commit-analyzer');
128
+ });
129
+
130
+ it('gives all of them the same parser options, so notes and version bumps agree', () => {
131
+ const [[, first]] = parsingPlugins;
132
+
133
+ // The plugin name rides along in the assertion so a failure says which one drifted.
134
+ for (const [name, opts] of parsingPlugins) {
135
+ expect([name, opts]).toEqual([name, first]);
136
+ }
137
+ });
138
+ });
139
+
140
+ describe.each(parsingPlugins)('%s', (_pluginName, opts) => {
141
+ it.each(acceptedHeaders)('parses $name', ({ header, type, scope }) => {
142
+ expect(parseHeader(opts, header)).toMatchObject({ type, scope, subject: expect.any(String) });
143
+ });
144
+
145
+ it('reads a comma scope as one scope, leaving the subject whole', () => {
146
+ expect(parseHeader(opts, commaScopeHeader)?.subject).toBe(
147
+ 'drive options from an async [dataSource] (#312)'
148
+ );
149
+ });
150
+
151
+ it('recognises a breaking change whose scope contains a comma', () => {
152
+ const breaking = new RegExp(opts.breakingHeaderPattern).exec(
153
+ 'feat(autocomplete,chip-input)!: options are now a dataSource'
154
+ );
155
+
156
+ expect(breaking?.[1]).toBe('feat');
157
+ expect(breaking?.[2]).toBe('autocomplete,chip-input');
158
+ });
159
+ });
160
+
161
+ describe('the PR title check and the release step', () => {
162
+ it.each(acceptedHeaders)('agree on $name', ({ header, type }) => {
163
+ expect(prTitleCheck.test(header)).toBe(true);
164
+
165
+ for (const [, opts] of parsingPlugins) {
166
+ expect(parseHeader(opts, header)).toMatchObject({ type });
167
+ }
168
+ });
169
+
170
+ it('agree on every scope character the check allows, not a narrower set', () => {
171
+ // The check's scope is `\([^)]+\)`. A release pattern with a narrower scope class is
172
+ // what produced #315: the title passed, the commit merged, the release dropped it.
173
+ const scopes = ['a,b', 'a, b', 'a/b', 'a.b', 'a-b', 'a b', 'a:b', 'a+b', 'a&b', 'a#b', '@scope/pkg'];
174
+
175
+ for (const scope of scopes) {
176
+ const header = `feat(${scope}): a subject`;
177
+
178
+ expect(prTitleCheck.test(header)).toBe(true);
179
+
180
+ for (const [, opts] of parsingPlugins) {
181
+ expect(parseHeader(opts, header)).toEqual({
182
+ type: 'feat',
183
+ scope,
184
+ subject: 'a subject',
185
+ });
186
+ }
187
+ }
188
+ });
189
+ });
@@ -0,0 +1,14 @@
1
+ import type { Config } from 'jest';
2
+
3
+ const config: Config = {
4
+ testEnvironment: 'node',
5
+ transform: {
6
+ '^.+\\.tsx?$': ['ts-jest', {
7
+ tsconfig: '<rootDir>/tsconfig.json',
8
+ }],
9
+ },
10
+ testMatch: ['<rootDir>/**/*.spec.ts'],
11
+ moduleFileExtensions: ['ts', 'js', 'json'],
12
+ };
13
+
14
+ export default config;
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "esModuleInterop": true,
7
+ "isolatedModules": true,
8
+ "strict": true,
9
+ "types": ["jest", "node"],
10
+ "rootDir": ".",
11
+ "outDir": "../../dist/out-tsc/scripts"
12
+ },
13
+ "include": ["**/*.ts"]
14
+ }
@@ -9686,6 +9686,63 @@ declare class TnTableComponent<T = unknown> implements OnInit {
9686
9686
  * full list it holds, and call {@link clearSelection} once a batch action is done.
9687
9687
  */
9688
9688
  selectionKey: _angular_core.InputSignal<((row: T) => unknown) | undefined>;
9689
+ /**
9690
+ * Identifies a row for EXPANSION, so an open detail row survives a `dataSource` change.
9691
+ *
9692
+ * Detail rows are held by object identity, and a consumer that re-fetches hands back new
9693
+ * objects for the same rows — so without a key a background reload closes the row the
9694
+ * user opened, which is usually the moment its contents are worth watching (a running
9695
+ * job's logs, say). With a key set, the table re-points the open rows at the new objects
9696
+ * instead of collapsing them.
9697
+ *
9698
+ * A row the new `data()` does not carry — another page, another tab, filtered out — is
9699
+ * kept, not dropped, and re-opens on its own once that row is listed again. Retention is
9700
+ * unconditional for the same reason it is for {@link selectionKey}: the table sees one
9701
+ * page and cannot tell a row that moved off-screen from one that is gone for good. Call
9702
+ * {@link clearExpansion} when the consumer knows it is gone.
9703
+ *
9704
+ * Without it the table keeps its historical behaviour — any `dataSource` reference change
9705
+ * collapses every open row.
9706
+ *
9707
+ * With a key set, drive expansion through {@link toggleRowExpansion}, {@link expandRow}
9708
+ * and {@link clearExpansion} rather than writing {@link expandedRows} directly, or the
9709
+ * retained set and the visible set drift apart.
9710
+ *
9711
+ * Pass a key that identifies the ROW, not its position, and bind a stable member rather
9712
+ * than writing the function in the template: `[expansionKey]="rowKey"` for a
9713
+ * `rowKey = (row: Job) => row.id` on the host.
9714
+ */
9715
+ expansionKey: _angular_core.InputSignal<((row: T) => unknown) | undefined>;
9716
+ /**
9717
+ * Test id for each ROW, as a function of the row and its index.
9718
+ *
9719
+ * The table writes no id on a `<tr>` of its own, and a cell body is free to be bare
9720
+ * interpolation — `{{ row.username }}` renders no element the consumer can bind to. A list
9721
+ * built that way is unaddressable: an automated suite that has to open, select or delete one
9722
+ * specific row has nothing to select it by but position or cell text, both of which change
9723
+ * under the test. Set this and every row carries `row-<base>` on the `<tr>`, and on its card
9724
+ * in card mode, through the same {@link TnTestIdDirective} as every other id the library
9725
+ * emits — so it honours {@link TN_TEST_ATTR} and is kebab-cased by {@link composeTestId}.
9726
+ *
9727
+ * Pass the base only, not the `row-` prefix, and identify the ROW rather than its position,
9728
+ * the way {@link selectionKey} and {@link expansionKey} do — an index-derived id renames every
9729
+ * row below the one that was deleted. The index is passed for the case where the data genuinely
9730
+ * has nothing unique in it.
9731
+ *
9732
+ * Bind a stable member rather than writing the function in the template, since Angular's
9733
+ * expression grammar has no arrow functions:
9734
+ * `[rowTestId]="rowTestId"` for a `rowTestId = (row: User) => row.username` on the host.
9735
+ *
9736
+ * ```html
9737
+ * <tn-table [dataSource]="users" [rowTestId]="rowTestId" />
9738
+ * <!-- <tr data-testid="row-jane-doe"> -->
9739
+ * ```
9740
+ *
9741
+ * Cells are not covered by this: a suite that has to read one cell of a row still needs an id
9742
+ * on the element that cell renders. Tagging the row is what makes the row itself reachable —
9743
+ * which is the part a consumer cannot add from the outside.
9744
+ */
9745
+ rowTestId: _angular_core.InputSignal<((row: T, index: number) => TnTestIdValue) | undefined>;
9689
9746
  emptyMessage: _angular_core.InputSignal<string>;
9690
9747
  /**
9691
9748
  * Optional second line under `emptyMessage`, for empty states that carry both a headline and an
@@ -9897,12 +9954,25 @@ declare class TnTableComponent<T = unknown> implements OnInit {
9897
9954
  sortColumn: _angular_core.ModelSignal<string>;
9898
9955
  sortDirection: _angular_core.ModelSignal<"" | "desc" | "asc">;
9899
9956
  /**
9900
- * Set of currently expanded row references.
9901
- * Note: uses object identity. If the consumer replaces the data array
9902
- * (e.g. after sorting), expanded state is lost. A future key-based
9903
- * approach could address this.
9957
+ * The rows of the CURRENT `data()` whose detail row is open, by object identity.
9958
+ *
9959
+ * Replacing the data array collapses everything unless {@link expansionKey} is set, in
9960
+ * which case this is the visible slice of {@link expandedByKey} and survives the swap.
9961
+ *
9962
+ * Writable for consumers that predate {@link expansionKey}. **Adding a key to a consumer
9963
+ * that writes this signal directly is a migration, not a drop-in**: a write here closes
9964
+ * what is on screen but never touches the retained map, so every row whose key is still in
9965
+ * it re-opens on the next reconcile — the one it just closed included. Move those writes to
9966
+ * {@link expandRow}, {@link toggleRowExpansion} and {@link clearExpansion}, which keep the
9967
+ * two in step.
9904
9968
  */
9905
9969
  expandedRows: _angular_core.WritableSignal<Set<unknown>>;
9970
+ /**
9971
+ * Every open detail row keyed by {@link expansionKey}, including rows no longer in
9972
+ * `data()`. Empty (and unused) when no key is set. Holds the row objects so a row that
9973
+ * comes back can be re-pointed at the object the new data carries.
9974
+ */
9975
+ private expandedByKey;
9906
9976
  private static instanceCount;
9907
9977
  private readonly instanceId;
9908
9978
  /**
@@ -9948,6 +10018,16 @@ declare class TnTableComponent<T = unknown> implements OnInit {
9948
10018
  /** True when the layout should keep the table but pin edge columns and scroll. */
9949
10019
  isScrollMode: Signal<boolean>;
9950
10020
  data: Signal<T[]>;
10021
+ /**
10022
+ * The resolved base for each row, by index.
10023
+ *
10024
+ * Computed once per `data()` change rather than called per row per change-detection pass: a
10025
+ * base is usually an array, every call builds a new one, and a new reference restarts the
10026
+ * directive's effect — so binding the call directly would rewrite every row's attribute on
10027
+ * every pass. Recomputing here on the signal the rows are rendered from keeps the value
10028
+ * identical between passes, and correct after a reload hands back new row objects.
10029
+ */
10030
+ protected readonly rowTestIds: Signal<TnTestIdValue[]>;
9951
10031
  effectiveDisplayedColumns: Signal<string[]>;
9952
10032
  /**
9953
10033
  * Every column the table actually renders, including the trailing actions column, which
@@ -9999,6 +10079,26 @@ declare class TnTableComponent<T = unknown> implements OnInit {
9999
10079
  */
10000
10080
  canExpandRow(row: T): boolean;
10001
10081
  toggleRowExpansion(row: T): void;
10082
+ /**
10083
+ * Whether `row`'s detail row counts as open for a toggle. With {@link expansionKey} that is
10084
+ * the retained map, which includes rows this page is not showing; without one it is object
10085
+ * identity against the visible set. Distinct from {@link isRowExpanded}, which drives
10086
+ * rendering and must therefore stay about the row in front of the user.
10087
+ */
10088
+ private isExpansionOpen;
10089
+ /**
10090
+ * Opens `row`'s detail row programmatically — the same thing a chevron click does, for a
10091
+ * consumer restoring expansion from somewhere else (a `?jobId=` query parameter, say).
10092
+ * Honours `singleExpand` and keeps {@link expansionKey}'s retained set in step, which
10093
+ * writing {@link expandedRows} directly does not.
10094
+ */
10095
+ expandRow(row: T): void;
10096
+ /**
10097
+ * Collapses every detail row, including rows retained under {@link expansionKey} that are
10098
+ * not on screen. `expandedRows.set(new Set())` only closes what is visible, so with a key
10099
+ * set the off-screen rows would re-open on the next reconcile.
10100
+ */
10101
+ clearExpansion(): void;
10002
10102
  isRowExpanded(row: T): boolean;
10003
10103
  /**
10004
10104
  * Whether the row element itself acts as the expand/collapse control. Requires `clickable` —
@@ -10226,7 +10326,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
10226
10326
  getCardLabel(column: string): string;
10227
10327
  getCellValue(row: T, column: string): unknown;
10228
10328
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTableComponent<any>, never>;
10229
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTableComponent<any>, "tn-table", never, { "dataSource": { "alias": "dataSource"; "required": false; "isSignal": true; }; "displayedColumns": { "alias": "displayedColumns"; "required": false; "isSignal": true; }; "trackBy": { "alias": "trackBy"; "required": false; "isSignal": true; }; "selectionKey": { "alias": "selectionKey"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "emptyDescription": { "alias": "emptyDescription"; "required": false; "isSignal": true; }; "emptyIcon": { "alias": "emptyIcon"; "required": false; "isSignal": true; }; "scrollRegionAriaLabel": { "alias": "scrollRegionAriaLabel"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "expandable": { "alias": "expandable"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; "isRowExpandable": { "alias": "isRowExpandable"; "required": false; "isSignal": true; }; "activeRow": { "alias": "activeRow"; "required": false; "isSignal": true; }; "activeWhen": { "alias": "activeWhen"; "required": false; "isSignal": true; }; "activeBg": { "alias": "activeBg"; "required": false; "isSignal": true; }; "activeIndicator": { "alias": "activeIndicator"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingMessage": { "alias": "loadingMessage"; "required": false; "isSignal": true; }; "clickable": { "alias": "clickable"; "required": false; "isSignal": true; }; "expandOnRowClick": { "alias": "expandOnRowClick"; "required": false; "isSignal": true; }; "singleExpand": { "alias": "singleExpand"; "required": false; "isSignal": true; }; "fixedLayout": { "alias": "fixedLayout"; "required": false; "isSignal": true; }; "minColumnWidth": { "alias": "minColumnWidth"; "required": false; "isSignal": true; }; "minWidth": { "alias": "minWidth"; "required": false; "isSignal": true; }; "mobileLayout": { "alias": "mobileLayout"; "required": false; "isSignal": true; }; "cardBreakpoint": { "alias": "cardBreakpoint"; "required": false; "isSignal": true; }; "cardPrimaryCount": { "alias": "cardPrimaryCount"; "required": false; "isSignal": true; }; "sortColumn": { "alias": "sortColumn"; "required": false; "isSignal": true; }; "sortDirection": { "alias": "sortDirection"; "required": false; "isSignal": true; }; }, { "sortChange": "sortChange"; "selectionChange": "selectionChange"; "rowClick": "rowClick"; "rowDoubleClick": "rowDoubleClick"; "sortColumn": "sortColumnChange"; "sortDirection": "sortDirectionChange"; }, ["columnDefs", "detailRowDef", "rowActionsDef"], never, true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
10329
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTableComponent<any>, "tn-table", never, { "dataSource": { "alias": "dataSource"; "required": false; "isSignal": true; }; "displayedColumns": { "alias": "displayedColumns"; "required": false; "isSignal": true; }; "trackBy": { "alias": "trackBy"; "required": false; "isSignal": true; }; "selectionKey": { "alias": "selectionKey"; "required": false; "isSignal": true; }; "expansionKey": { "alias": "expansionKey"; "required": false; "isSignal": true; }; "rowTestId": { "alias": "rowTestId"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "emptyDescription": { "alias": "emptyDescription"; "required": false; "isSignal": true; }; "emptyIcon": { "alias": "emptyIcon"; "required": false; "isSignal": true; }; "scrollRegionAriaLabel": { "alias": "scrollRegionAriaLabel"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "expandable": { "alias": "expandable"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; "isRowExpandable": { "alias": "isRowExpandable"; "required": false; "isSignal": true; }; "activeRow": { "alias": "activeRow"; "required": false; "isSignal": true; }; "activeWhen": { "alias": "activeWhen"; "required": false; "isSignal": true; }; "activeBg": { "alias": "activeBg"; "required": false; "isSignal": true; }; "activeIndicator": { "alias": "activeIndicator"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingMessage": { "alias": "loadingMessage"; "required": false; "isSignal": true; }; "clickable": { "alias": "clickable"; "required": false; "isSignal": true; }; "expandOnRowClick": { "alias": "expandOnRowClick"; "required": false; "isSignal": true; }; "singleExpand": { "alias": "singleExpand"; "required": false; "isSignal": true; }; "fixedLayout": { "alias": "fixedLayout"; "required": false; "isSignal": true; }; "minColumnWidth": { "alias": "minColumnWidth"; "required": false; "isSignal": true; }; "minWidth": { "alias": "minWidth"; "required": false; "isSignal": true; }; "mobileLayout": { "alias": "mobileLayout"; "required": false; "isSignal": true; }; "cardBreakpoint": { "alias": "cardBreakpoint"; "required": false; "isSignal": true; }; "cardPrimaryCount": { "alias": "cardPrimaryCount"; "required": false; "isSignal": true; }; "sortColumn": { "alias": "sortColumn"; "required": false; "isSignal": true; }; "sortDirection": { "alias": "sortDirection"; "required": false; "isSignal": true; }; }, { "sortChange": "sortChange"; "selectionChange": "selectionChange"; "rowClick": "rowClick"; "rowDoubleClick": "rowDoubleClick"; "sortColumn": "sortColumnChange"; "sortDirection": "sortDirectionChange"; }, ["columnDefs", "detailRowDef", "rowActionsDef"], never, true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
10230
10330
  }
10231
10331
 
10232
10332
  /**
@@ -10431,6 +10531,17 @@ declare class TnTableHarness extends ComponentHarness {
10431
10531
  * @returns Promise resolving to true if the loading overlay is visible.
10432
10532
  */
10433
10533
  isLoading(): Promise<boolean>;
10534
+ /**
10535
+ * Gets the test id written on a data row by `[rowTestId]`.
10536
+ *
10537
+ * Layout-aware, like the rest of the row queries: reads the card in the card layout, which
10538
+ * carries the same id. Reads `data-testid` first and falls through to `data-test`, so it
10539
+ * answers the same either way the consumer configured `TN_TEST_ATTR`.
10540
+ *
10541
+ * @param rowIndex Zero-based index of the data row (or card).
10542
+ * @returns Promise resolving to the row's test id, or null when `[rowTestId]` is unset.
10543
+ */
10544
+ getRowTestId(rowIndex: number): Promise<string | null>;
10434
10545
  /**
10435
10546
  * Checks if a data row is currently marked active.
10436
10547
  *