@ngneers/controls-playwright 0.0.1-pre1

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 (164) hide show
  1. package/README.md +20 -0
  2. package/awesome-publish.config.ts +6 -0
  3. package/dist/components/accordion.d.ts +21 -0
  4. package/dist/components/accordion.d.ts.map +1 -0
  5. package/dist/components/accordion.js +42 -0
  6. package/dist/components/avatar.d.ts +9 -0
  7. package/dist/components/avatar.d.ts.map +1 -0
  8. package/dist/components/avatar.js +16 -0
  9. package/dist/components/breadcrumb.d.ts +20 -0
  10. package/dist/components/breadcrumb.d.ts.map +1 -0
  11. package/dist/components/breadcrumb.js +15 -0
  12. package/dist/components/button-group.d.ts +10 -0
  13. package/dist/components/button-group.d.ts.map +1 -0
  14. package/dist/components/button-group.js +16 -0
  15. package/dist/components/button.d.ts +6 -0
  16. package/dist/components/button.d.ts.map +1 -0
  17. package/dist/components/button.js +6 -0
  18. package/dist/components/calendar.d.ts +101 -0
  19. package/dist/components/calendar.d.ts.map +1 -0
  20. package/dist/components/calendar.js +56 -0
  21. package/dist/components/chip.d.ts +15 -0
  22. package/dist/components/chip.d.ts.map +1 -0
  23. package/dist/components/chip.js +38 -0
  24. package/dist/components/edit-inplace.d.ts +18 -0
  25. package/dist/components/edit-inplace.d.ts.map +1 -0
  26. package/dist/components/edit-inplace.js +29 -0
  27. package/dist/components/filter.d.ts +186 -0
  28. package/dist/components/filter.d.ts.map +1 -0
  29. package/dist/components/filter.js +55 -0
  30. package/dist/components/hint.d.ts +10 -0
  31. package/dist/components/hint.d.ts.map +1 -0
  32. package/dist/components/hint.js +22 -0
  33. package/dist/components/index.d.ts +38 -0
  34. package/dist/components/index.d.ts.map +1 -0
  35. package/dist/components/index.js +37 -0
  36. package/dist/components/inplace.d.ts +12 -0
  37. package/dist/components/inplace.d.ts.map +1 -0
  38. package/dist/components/inplace.js +29 -0
  39. package/dist/components/input-field.d.ts +12 -0
  40. package/dist/components/input-field.d.ts.map +1 -0
  41. package/dist/components/input-field.js +11 -0
  42. package/dist/components/input.d.ts +12 -0
  43. package/dist/components/input.d.ts.map +1 -0
  44. package/dist/components/input.js +25 -0
  45. package/dist/components/item-view.d.ts +18 -0
  46. package/dist/components/item-view.d.ts.map +1 -0
  47. package/dist/components/item-view.js +39 -0
  48. package/dist/components/list-box.d.ts +20 -0
  49. package/dist/components/list-box.d.ts.map +1 -0
  50. package/dist/components/list-box.js +24 -0
  51. package/dist/components/mask-input.d.ts +62 -0
  52. package/dist/components/mask-input.d.ts.map +1 -0
  53. package/dist/components/mask-input.js +109 -0
  54. package/dist/components/menu.d.ts +16 -0
  55. package/dist/components/menu.d.ts.map +1 -0
  56. package/dist/components/menu.js +26 -0
  57. package/dist/components/message.d.ts +10 -0
  58. package/dist/components/message.d.ts.map +1 -0
  59. package/dist/components/message.js +22 -0
  60. package/dist/components/popover.d.ts +12 -0
  61. package/dist/components/popover.d.ts.map +1 -0
  62. package/dist/components/popover.js +33 -0
  63. package/dist/components/progress.d.ts +17 -0
  64. package/dist/components/progress.d.ts.map +1 -0
  65. package/dist/components/progress.js +46 -0
  66. package/dist/components/radio.d.ts +16 -0
  67. package/dist/components/radio.d.ts.map +1 -0
  68. package/dist/components/radio.js +35 -0
  69. package/dist/components/scroller.d.ts +20 -0
  70. package/dist/components/scroller.d.ts.map +1 -0
  71. package/dist/components/scroller.js +58 -0
  72. package/dist/components/select-button.d.ts +30 -0
  73. package/dist/components/select-button.d.ts.map +1 -0
  74. package/dist/components/select-button.js +46 -0
  75. package/dist/components/select.d.ts +46 -0
  76. package/dist/components/select.d.ts.map +1 -0
  77. package/dist/components/select.js +74 -0
  78. package/dist/components/slider.d.ts +26 -0
  79. package/dist/components/slider.d.ts.map +1 -0
  80. package/dist/components/slider.js +73 -0
  81. package/dist/components/snackbar.d.ts +27 -0
  82. package/dist/components/snackbar.d.ts.map +1 -0
  83. package/dist/components/snackbar.js +70 -0
  84. package/dist/components/spinner.d.ts +11 -0
  85. package/dist/components/spinner.d.ts.map +1 -0
  86. package/dist/components/spinner.js +22 -0
  87. package/dist/components/state.d.ts +31 -0
  88. package/dist/components/state.d.ts.map +1 -0
  89. package/dist/components/state.js +31 -0
  90. package/dist/components/switch.d.ts +12 -0
  91. package/dist/components/switch.d.ts.map +1 -0
  92. package/dist/components/switch.js +39 -0
  93. package/dist/components/tabs.d.ts +22 -0
  94. package/dist/components/tabs.d.ts.map +1 -0
  95. package/dist/components/tabs.js +42 -0
  96. package/dist/components/tag.d.ts +10 -0
  97. package/dist/components/tag.d.ts.map +1 -0
  98. package/dist/components/tag.js +22 -0
  99. package/dist/components/toast.d.ts +27 -0
  100. package/dist/components/toast.d.ts.map +1 -0
  101. package/dist/components/toast.js +68 -0
  102. package/dist/components/toggle-button.d.ts +10 -0
  103. package/dist/components/toggle-button.d.ts.map +1 -0
  104. package/dist/components/toggle-button.js +29 -0
  105. package/dist/components/tooltip.d.ts +12 -0
  106. package/dist/components/tooltip.d.ts.map +1 -0
  107. package/dist/components/tooltip.js +32 -0
  108. package/dist/components/tree.d.ts +47 -0
  109. package/dist/components/tree.d.ts.map +1 -0
  110. package/dist/components/tree.js +85 -0
  111. package/dist/components/upload.d.ts +27 -0
  112. package/dist/components/upload.d.ts.map +1 -0
  113. package/dist/components/upload.js +47 -0
  114. package/dist/index.d.ts +2 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +1 -0
  117. package/dist/utils/classes.d.ts +119 -0
  118. package/dist/utils/classes.d.ts.map +1 -0
  119. package/dist/utils/classes.js +31 -0
  120. package/dist/utils/theme.d.ts +3 -0
  121. package/dist/utils/theme.d.ts.map +1 -0
  122. package/dist/utils/theme.js +41 -0
  123. package/package.json +38 -0
  124. package/src/components/accordion.ts +50 -0
  125. package/src/components/avatar.ts +17 -0
  126. package/src/components/breadcrumb.ts +17 -0
  127. package/src/components/button-group.ts +18 -0
  128. package/src/components/button.ts +5 -0
  129. package/src/components/calendar.ts +67 -0
  130. package/src/components/chip.ts +43 -0
  131. package/src/components/edit-inplace.ts +34 -0
  132. package/src/components/filter.ts +70 -0
  133. package/src/components/hint.ts +25 -0
  134. package/src/components/index.ts +37 -0
  135. package/src/components/inplace.ts +33 -0
  136. package/src/components/input-field.ts +16 -0
  137. package/src/components/input.ts +32 -0
  138. package/src/components/item-view.ts +48 -0
  139. package/src/components/list-box.ts +25 -0
  140. package/src/components/mask-input.ts +119 -0
  141. package/src/components/menu.ts +29 -0
  142. package/src/components/message.ts +25 -0
  143. package/src/components/popover.ts +39 -0
  144. package/src/components/progress.ts +53 -0
  145. package/src/components/radio.ts +42 -0
  146. package/src/components/scroller.ts +68 -0
  147. package/src/components/select-button.ts +57 -0
  148. package/src/components/select.ts +81 -0
  149. package/src/components/slider.ts +84 -0
  150. package/src/components/snackbar.ts +84 -0
  151. package/src/components/spinner.ts +27 -0
  152. package/src/components/state.ts +37 -0
  153. package/src/components/switch.ts +43 -0
  154. package/src/components/tabs.ts +50 -0
  155. package/src/components/tag.ts +25 -0
  156. package/src/components/toast.ts +82 -0
  157. package/src/components/toggle-button.ts +33 -0
  158. package/src/components/tooltip.ts +38 -0
  159. package/src/components/tree.ts +98 -0
  160. package/src/components/upload.ts +63 -0
  161. package/src/index.ts +1 -0
  162. package/src/utils/classes.ts +32 -0
  163. package/src/utils/theme.ts +44 -0
  164. package/tsconfig.lib.json +20 -0
@@ -0,0 +1,119 @@
1
+ import { type Locator, expect } from '@playwright/test';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { maskInputControlTemplate } from '@ngneers/controls-themes/templates/mask-input';
4
+
5
+ /**
6
+ * Playwright harness for `ngn-mask-input` (v2).
7
+ *
8
+ * ## Value assertion
9
+ * The control's `value` model (the serialized string, or `null` when incomplete)
10
+ * is NOT written into any DOM element — the proxy input stays empty at all times.
11
+ * E2E tests must assert the emitted value via a bound echo element rendered by the
12
+ * test component (e.g. `<span data-testid="value">{{ inputs().value }}</span>`).
13
+ * This harness intentionally does NOT expose an `expectValue` method.
14
+ */
15
+ export class NgnMaskInputHarness {
16
+ public readonly classes = themeClasses(maskInputControlTemplate);
17
+
18
+ /**
19
+ * The visually-hidden proxy input — this is the element that receives focus
20
+ * and keyboard events. All `press` / `pressSequentially` calls target it.
21
+ */
22
+ public readonly proxy: Locator;
23
+
24
+ /**
25
+ * All `[role="spinbutton"]` section spans (both filled and placeholder state).
26
+ */
27
+ public readonly sections: Locator;
28
+
29
+ constructor(private readonly locator: Locator) {
30
+ // The proxy is the only <input> inside the control.
31
+ this.proxy = locator.locator('input').first();
32
+ this.sections = locator.locator('[role="spinbutton"]');
33
+ }
34
+
35
+ /** Focus the proxy input so subsequent key events are delivered to the control. */
36
+ public async focus(): Promise<void> {
37
+ await this.proxy.focus();
38
+ }
39
+
40
+ /** Press a single key on the proxy input (e.g. 'ArrowRight', 'Backspace', '1'). */
41
+ public async press(key: string): Promise<void> {
42
+ await this.proxy.focus();
43
+ await this.proxy.press(key);
44
+ }
45
+
46
+ /** Type a string of characters into the proxy input one by one, with a small delay. */
47
+ public async pressSequentially(text: string): Promise<void> {
48
+ await this.proxy.focus();
49
+ await this.proxy.pressSequentially(text, { delay: 5 });
50
+ }
51
+
52
+ /**
53
+ * Assert the visible rendered text of the control — the concatenation (with no
54
+ * separator) of all section, section-placeholder, and separator spans in DOM order.
55
+ * Screen-reader-only (`sr-only`) spans are excluded.
56
+ *
57
+ * Example: for a time mask `HH:MM:SS` with `12` entered in the first section
58
+ * the visible text is `'12:MM:SS'`.
59
+ *
60
+ * Uses Playwright's auto-retry (`expect.poll`) so it waits for Angular to render.
61
+ */
62
+ public expectText(visible: string): Promise<void> {
63
+ const visibleSelector = [
64
+ this.classes['section'],
65
+ this.classes['section-placeholder'],
66
+ this.classes.separator,
67
+ ].join(', ');
68
+
69
+ return expect
70
+ .poll(async () => {
71
+ const texts = await this.locator.locator(visibleSelector).allInnerTexts();
72
+ return texts.join('');
73
+ })
74
+ .toEqual(visible);
75
+ }
76
+
77
+ /**
78
+ * Returns the value of `aria-activedescendant` from the proxy input, which
79
+ * is the `id` of the currently focused section span. Returns `null` when the
80
+ * attribute is absent.
81
+ */
82
+ public async activeDescendantId(): Promise<string | null> {
83
+ const val = await this.proxy.getAttribute('aria-activedescendant');
84
+ return val ?? null;
85
+ }
86
+
87
+ /**
88
+ * Assert the innerText of the currently active section span.
89
+ * Useful for verifying which section is focused and what value it shows.
90
+ */
91
+ public async expectActiveText(text: string): Promise<void> {
92
+ const id = await this.activeDescendantId();
93
+ if (id === null) {
94
+ throw new Error('No aria-activedescendant on proxy — no active section');
95
+ }
96
+ const activeSpan = this.locator.locator(`#${id}`);
97
+ await expect(activeSpan).toHaveText(text, { useInnerText: true });
98
+ }
99
+
100
+ /**
101
+ * Clear all sections by focusing the proxy, pressing Home to jump to the first
102
+ * section, then pressing Backspace twice per section (once to clear the value,
103
+ * once to step back if it was already empty — harmless no-op).
104
+ *
105
+ * This is intentionally blunt: it over-presses rather than inspecting state.
106
+ */
107
+ public async clear(): Promise<void> {
108
+ await this.proxy.focus();
109
+ // Start from the LAST section: Backspace clears the active section then steps
110
+ // to the previous one, so walking from the end clears every section. Starting
111
+ // from Home would get stuck on section 0 (Backspace there is a no-op).
112
+ await this.proxy.press('End');
113
+ const count = await this.sections.count();
114
+ for (let i = 0; i < count; i++) {
115
+ await this.proxy.press('Backspace');
116
+ await this.proxy.press('Backspace');
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,29 @@
1
+ import { expect, type Locator } from '@playwright/test';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { menuControlTemplate } from '@ngneers/controls-themes/templates/menu';
4
+ import { NgnPopoverHarness } from './popover';
5
+
6
+ export class NgnMenuHarness {
7
+ public readonly classes = themeClasses(menuControlTemplate);
8
+ public readonly popover: NgnPopoverHarness;
9
+ public readonly item: Locator;
10
+
11
+ constructor(public locator: Locator) {
12
+ this.popover = new NgnPopoverHarness(locator.locator(this.classes['popover']['root']));
13
+ this.item = locator.locator(this.classes['item']);
14
+ }
15
+
16
+ public async expectItemCount(count: number) {
17
+ await expect(this.item).toHaveCount(count);
18
+ }
19
+
20
+ public async getItemByIndex(index: number) {
21
+ return this.item.nth(index);
22
+ }
23
+
24
+ public async openChildMenuByIndex(index: number) {
25
+ const item = this.item.nth(index);
26
+ await item.click();
27
+ return new NgnMenuHarness(item.locator('ngn-menu').first());
28
+ }
29
+ }
@@ -0,0 +1,25 @@
1
+ import { messageControlTemplate } from '@ngneers/controls-themes/templates/message';
2
+ import { themeClasses } from '../utils/theme';
3
+ import test, { expect, type Locator } from '@playwright/test';
4
+
5
+ export class NgnMessageHarness {
6
+ public readonly classes = themeClasses(messageControlTemplate);
7
+
8
+ public readonly locator: Locator;
9
+ public readonly content: Locator;
10
+ public readonly icon: Locator;
11
+
12
+ constructor(locator: Locator) {
13
+ this.locator = locator;
14
+ this.content = locator.locator(this.classes.content);
15
+ this.icon = locator.locator(this.classes.icon);
16
+ }
17
+
18
+ public async expectIcon(hasIcon: boolean) {
19
+ if (hasIcon) {
20
+ await expect(this.icon).toBeVisible();
21
+ } else {
22
+ await expect(this.icon).not.toBeVisible();
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ import { type Locator, expect } from '@playwright/test';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { popoverControlTemplate } from '@ngneers/controls-themes/templates/popover';
4
+
5
+ export class NgnPopoverHarness {
6
+ public readonly classes = themeClasses(popoverControlTemplate);
7
+
8
+ private readonly _ngnLazyCacher: Locator;
9
+ private readonly _contentWrapper: Locator;
10
+ private readonly _content: Locator;
11
+
12
+ constructor(locator: Locator) {
13
+ this._ngnLazyCacher = locator.locator('ngn-defer');
14
+ this._contentWrapper = locator.locator(this.classes.content);
15
+ this._content = this._ngnLazyCacher.locator('> *');
16
+ }
17
+
18
+ public get content(): Locator {
19
+ return this._content;
20
+ }
21
+
22
+ public async expectOpened() {
23
+ await expect(this._contentWrapper).toBeVisible();
24
+ }
25
+
26
+ public async expectRendered(rendered = true): Promise<void> {
27
+ await expect(this._ngnLazyCacher).toBeAttached();
28
+ await expect(async () => {
29
+ const el = await this._ngnLazyCacher.elementHandle();
30
+ const hasContent =
31
+ !!(await el?.innerText()) || !!(await el?.evaluate(e => e.childElementCount));
32
+ if (rendered) {
33
+ expect(hasContent).toBeTruthy();
34
+ } else {
35
+ expect(hasContent).toBeFalsy();
36
+ }
37
+ }).toPass({ timeout: 2000 });
38
+ }
39
+ }
@@ -0,0 +1,53 @@
1
+ import { progressControlTemplate } from '@ngneers/controls-themes/templates/progress';
2
+ import { themeClasses } from '../utils/theme';
3
+ import test, { expect, type Locator } from '@playwright/test';
4
+
5
+ export class NgnProgressHarness {
6
+ public readonly classes = themeClasses(progressControlTemplate);
7
+
8
+ public readonly locator: Locator;
9
+ public readonly track: Locator;
10
+ public readonly fill: Locator;
11
+ public readonly fill2: Locator;
12
+ public readonly svg: Locator;
13
+
14
+ constructor(locator: Locator) {
15
+ this.locator = locator;
16
+ this.track = locator.locator(this.classes.track);
17
+ this.fill = locator.locator(this.classes.fill);
18
+ this.fill2 = locator.locator(this.classes.fill2);
19
+ this.svg = locator.locator(this.classes.svg);
20
+ }
21
+
22
+ public async expectValue(value: number) {
23
+ await expect(this.locator).toHaveAttribute('aria-valuenow', value.toString());
24
+ }
25
+
26
+ public async expectMin(min: number) {
27
+ await expect(this.locator).toHaveAttribute('aria-valuemin', min.toString());
28
+ }
29
+
30
+ public async expectMax(max: number) {
31
+ await expect(this.locator).toHaveAttribute('aria-valuemax', max.toString());
32
+ }
33
+
34
+ public async expectVisible() {
35
+ await expect(this.locator).toBeVisible();
36
+ }
37
+
38
+ public async expectIndeterminate(indeterminate: boolean) {
39
+ if (indeterminate) {
40
+ await expect(this.fill2).toBeAttached();
41
+ } else {
42
+ await expect(this.fill2).not.toBeAttached();
43
+ }
44
+ }
45
+
46
+ public async expectCircular(circular: boolean) {
47
+ if (circular) {
48
+ await expect(this.svg).toBeVisible();
49
+ } else {
50
+ await expect(this.svg).not.toBeAttached();
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,42 @@
1
+ import { type Locator, expect } from '@playwright/test';
2
+
3
+ export class NgnRadioGroupHarness {
4
+ public readonly locator: Locator;
5
+ /** All radio options in the group, in DOM order. */
6
+ public readonly radios: Locator;
7
+
8
+ constructor(locator: Locator) {
9
+ this.locator = locator;
10
+ this.radios = locator.locator('ngn-radio');
11
+ }
12
+
13
+ public radio(index: number): Locator {
14
+ return this.radios.nth(index);
15
+ }
16
+
17
+ /** Click the radio at `index` to select it. */
18
+ public async select(index: number, force = false) {
19
+ await this.radio(index).click({ force });
20
+ }
21
+
22
+ public async expectSelected(index: number) {
23
+ await expect(this.radio(index)).toHaveAttribute('aria-checked', 'true');
24
+ }
25
+
26
+ public async expectNotSelected(index: number) {
27
+ await expect(this.radio(index)).toHaveAttribute('aria-checked', 'false');
28
+ }
29
+
30
+ public async expectDisabled(index: number, disabled = true) {
31
+ if (disabled) {
32
+ await expect(this.radio(index)).toHaveAttribute('aria-disabled', 'true');
33
+ } else {
34
+ await expect(this.radio(index)).not.toHaveAttribute('aria-disabled', 'true');
35
+ }
36
+ }
37
+
38
+ /** Focus the currently active radio (the group's single tab stop). */
39
+ public async focusActive() {
40
+ await this.locator.locator('ngn-radio[tabindex="0"]').focus();
41
+ }
42
+ }
@@ -0,0 +1,68 @@
1
+ import { expect, type Locator } from '@playwright/test';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { scrollerControlTemplate } from '@ngneers/controls-themes/templates/scroller';
4
+
5
+ export class NgnScrollerHarness {
6
+ public readonly classes = themeClasses(scrollerControlTemplate);
7
+ public readonly scrollarea: Locator;
8
+ public readonly item: Locator;
9
+ public readonly itemSticky: Locator;
10
+
11
+ constructor(public locator: Locator) {
12
+ this.scrollarea = locator;
13
+ this.item = locator.locator(`> :not(${this.classes['item-sticky']})`);
14
+ this.itemSticky = locator.locator(this.classes['item-sticky']);
15
+ }
16
+
17
+ public expectItemsCount(count: number): Promise<void> {
18
+ return expect(this.item).toHaveCount(count);
19
+ }
20
+
21
+ public expectStickyItemsCount(count: number): Promise<void> {
22
+ return expect(this.itemSticky).toHaveCount(count);
23
+ }
24
+
25
+ public expectItemsCountBetween(min: number, max: number): Promise<void> {
26
+ return expect(async () => {
27
+ const count = await this.item.count();
28
+ if (count < min || count > max) {
29
+ throw new Error(`Expected item count to be between ${min} and ${max}, but got ${count}`);
30
+ }
31
+ }).toPass();
32
+ }
33
+
34
+ public expectItemsTexts(texts: string[]): Promise<void> {
35
+ return expect(this.item).toHaveText(texts);
36
+ }
37
+
38
+ public expectStickyItemsTexts(texts: string[]): Promise<void> {
39
+ return expect(this.itemSticky).toHaveText(texts);
40
+ }
41
+
42
+ public clickItemByIndex(index: number): Promise<void> {
43
+ return this.item.nth(index).click();
44
+ }
45
+
46
+ public clickItemByText(text: string): Promise<void> {
47
+ return this.item.filter({ hasText: new RegExp(`^\\s*${text}\\s*$`, 'i') }).click();
48
+ }
49
+
50
+ public getItemByIndex(index: number): Locator {
51
+ return this.item.nth(index);
52
+ }
53
+
54
+ public getItemByText(text: string): Locator {
55
+ return this.item.filter({ hasText: new RegExp(`^\\s*${text}\\s*$`, 'i') });
56
+ }
57
+
58
+ public async scrollToIndex(index: number, itemHeight?: number): Promise<void> {
59
+ if (!itemHeight) {
60
+ await this.item.nth(index).scrollIntoViewIfNeeded();
61
+ return;
62
+ }
63
+ const scrollAmount = index * itemHeight;
64
+ await this.scrollarea.evaluate((el, amount) => {
65
+ el.scrollTo({ top: amount });
66
+ }, scrollAmount);
67
+ }
68
+ }
@@ -0,0 +1,57 @@
1
+ import { type Locator, expect } from '@playwright/test';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { selectButtonControlTemplate } from '@ngneers/controls-themes/templates/select-button';
4
+ import { NgnToggleButtonHarness } from './toggle-button';
5
+
6
+ export class NgnSelectButtonHarness {
7
+ public readonly classes = themeClasses(selectButtonControlTemplate);
8
+
9
+ public readonly locator: Locator;
10
+ public readonly buttons: Locator;
11
+
12
+ constructor(locator: Locator) {
13
+ this.locator = locator;
14
+ this.buttons = locator.locator('ngn-toggle-button');
15
+ }
16
+
17
+ public getButtonAt(index: number): NgnToggleButtonHarness {
18
+ return new NgnToggleButtonHarness(this.buttons.nth(index));
19
+ }
20
+
21
+ public getButtonByTestId(testId: string): NgnToggleButtonHarness {
22
+ return new NgnToggleButtonHarness(
23
+ this.locator.locator(`ngn-toggle-button[data-testid="${testId}"]`)
24
+ );
25
+ }
26
+
27
+ public async clickButtonAt(index: number, force = false) {
28
+ await this.getButtonAt(index).click(force);
29
+ }
30
+
31
+ public async expectButtonCount(count: number) {
32
+ await expect(this.buttons).toHaveCount(count);
33
+ }
34
+
35
+ public async expectSelectedAt(index: number) {
36
+ await this.getButtonAt(index).expectActive(true);
37
+ }
38
+
39
+ public async expectNoneSelected() {
40
+ const count = await this.buttons.count();
41
+ for (let i = 0; i < count; i++) {
42
+ await this.getButtonAt(i).expectActive(false);
43
+ }
44
+ }
45
+
46
+ public async expectDisabled(disabled: boolean) {
47
+ const innerButtons = this.locator.locator('button');
48
+ const count = await innerButtons.count();
49
+ for (let i = 0; i < count; i++) {
50
+ if (disabled) {
51
+ await expect(innerButtons.nth(i)).toBeDisabled();
52
+ } else {
53
+ await expect(innerButtons.nth(i)).not.toBeDisabled();
54
+ }
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,81 @@
1
+ import { expect, type Locator } from '@playwright/test';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { selectControlTemplate } from '@ngneers/controls-themes/templates/select';
4
+ import { NgnInputHarness } from './input';
5
+ import { NGN_CLASSES } from '../utils/classes';
6
+ import { NgnListBoxHarness } from './list-box';
7
+ import { NgnInputFieldHarness } from './input-field';
8
+ import { NgnItemViewHarness } from './item-view';
9
+
10
+ export class NgnSelectHarness {
11
+ public readonly classes = themeClasses(selectControlTemplate);
12
+ public readonly filter: NgnInputFieldHarness<{ input: NgnInputHarness }>;
13
+ public readonly filterIcon: Locator;
14
+ public readonly icon: Locator;
15
+ public readonly input: Locator;
16
+ public readonly inputEditable: NgnInputHarness;
17
+ public readonly listBox: NgnListBoxHarness;
18
+ public readonly popoverContent: Locator;
19
+ public readonly multipleItemView: NgnItemViewHarness;
20
+
21
+ constructor(public locator: Locator) {
22
+ this.filter = new NgnInputFieldHarness(locator.locator(this.classes['filter']['root']), l => ({
23
+ input: new NgnInputHarness(l.locator(NGN_CLASSES.input['root'])),
24
+ }));
25
+ this.filterIcon = locator.locator(this.classes['filter-icon']);
26
+ this.icon = locator.locator(this.classes['icon']);
27
+ this.input = locator.locator(this.classes['input']);
28
+ this.inputEditable = new NgnInputHarness(
29
+ locator.locator(`${this.classes['input-editable']} ${NGN_CLASSES.input['root']}`)
30
+ );
31
+ this.listBox = new NgnListBoxHarness(locator.locator(this.classes['list-box']['root']));
32
+ this.popoverContent = locator.locator(this.classes['popover-content']);
33
+ this.multipleItemView = new NgnItemViewHarness(locator.locator('ngn-item-view'));
34
+ }
35
+
36
+ public async expectOpened(opened = true) {
37
+ // Gate on the trigger's `aria-expanded` (bound to `popover.open()`), not just
38
+ // content visibility. `open()` is only set once the native popover has
39
+ // actually toggled in the top layer, whereas the content becomes CSS-visible
40
+ // a frame earlier — before the popover is a dismissible top-layer element.
41
+ // Waiting on visibility alone lets a follow-up keypress (e.g. Escape) race
42
+ // the open transition under load and get lost, leaving the popover stuck
43
+ // open. The `aria-expanded`/`aria-haspopup` attributes live on the inner
44
+ // combobox/input trigger, not the outer `input` wrapper.
45
+ const trigger = this.locator.locator('[aria-haspopup="listbox"]');
46
+ await expect(trigger).toHaveAttribute('aria-expanded', String(opened));
47
+ await expect(this.popoverContent).toBeVisible({ visible: opened });
48
+ }
49
+
50
+ public async open() {
51
+ await this.icon.click();
52
+ await this.expectOpened();
53
+ }
54
+
55
+ public async close() {
56
+ await this.icon.click();
57
+ await this.expectOpened(false);
58
+ }
59
+
60
+ public async expectSelectedItemText(text: string, editable = false) {
61
+ if (editable) {
62
+ await this.inputEditable.expectValue(text);
63
+ } else {
64
+ await expect(this.input).toHaveText(text, { useInnerText: true });
65
+ }
66
+ }
67
+
68
+ public async clickItemByText(text: string, expectClosed = true) {
69
+ await this.listBox.scroller.clickItemByText(text);
70
+ if (expectClosed) {
71
+ await this.expectOpened(false);
72
+ }
73
+ }
74
+
75
+ public async clickItemByIndex(index: number, expectClosed = true) {
76
+ await this.listBox.scroller.clickItemByIndex(index);
77
+ if (expectClosed) {
78
+ await this.expectOpened(false);
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,84 @@
1
+ import { sliderControlTemplate } from '@ngneers/controls-themes/templates/slider';
2
+ import { themeClasses } from '../utils/theme';
3
+ import test, { expect, type Locator } from '@playwright/test';
4
+
5
+ export class NgnSliderHarness {
6
+ public readonly classes = themeClasses(sliderControlTemplate);
7
+
8
+ public readonly locator: Locator;
9
+ public readonly track: Locator;
10
+ public readonly thumb: Locator;
11
+ public readonly fill: Locator;
12
+
13
+ constructor(locator: Locator) {
14
+ this.locator = locator;
15
+ this.track = locator.locator(this.classes.track);
16
+ this.thumb = locator.locator(this.classes.thumb);
17
+ this.fill = locator.locator(this.classes.fill);
18
+ }
19
+
20
+ public async expectValue(value: number) {
21
+ await expect(this.locator).toHaveAttribute('aria-valuenow', value.toString());
22
+ }
23
+
24
+ public async expectMin(min: number) {
25
+ await expect(this.locator).toHaveAttribute('aria-valuemin', min.toString());
26
+ }
27
+
28
+ public async expectMax(max: number) {
29
+ await expect(this.locator).toHaveAttribute('aria-valuemax', max.toString());
30
+ }
31
+
32
+ public async expectOrientation(orientation: 'horizontal' | 'vertical') {
33
+ await expect(this.locator).toHaveAttribute('aria-orientation', orientation);
34
+ }
35
+
36
+ public async expectReadonly(readonly: boolean) {
37
+ if (readonly) {
38
+ await expect(this.locator).toHaveAttribute('aria-readonly', 'true');
39
+ await expect(this.locator).toHaveAttribute('tabindex', '0');
40
+ } else {
41
+ await expect(this.locator).not.toHaveAttribute('aria-readonly');
42
+ }
43
+ }
44
+
45
+ public async expectDisabled(disabled: boolean) {
46
+ if (disabled) {
47
+ await expect(this.locator).toHaveAttribute('disabled');
48
+ await expect(this.locator).toHaveAttribute('tabindex', '-1');
49
+ } else {
50
+ await expect(this.locator).not.toHaveAttribute('disabled');
51
+ }
52
+ }
53
+
54
+ public async clickTrack(position: { x?: number; y?: number } = {}) {
55
+ const box = await this.track.boundingBox();
56
+ if (!box) {
57
+ throw new Error('Track not found');
58
+ }
59
+ const x = position.x !== undefined ? box.x + position.x : box.x + box.width / 2;
60
+ const y = position.y !== undefined ? box.y + position.y : box.y + box.height / 2;
61
+ await this.track.click({ position: { x: x - box.x, y: y - box.y } });
62
+ }
63
+
64
+ public async dragThumb(delta: { x?: number; y?: number }) {
65
+ await this.thumb.hover();
66
+ await this.locator.page().mouse.down();
67
+ const box = await this.thumb.boundingBox();
68
+ if (!box) {
69
+ throw new Error('Thumb not found');
70
+ }
71
+ await this.locator
72
+ .page()
73
+ .mouse.move(box.x + box.width / 2 + (delta.x || 0), box.y + box.height / 2 + (delta.y || 0));
74
+ await this.locator.page().mouse.up();
75
+ }
76
+
77
+ public async pressKey(key: string) {
78
+ await this.locator.press(key);
79
+ }
80
+
81
+ public async focus() {
82
+ await this.locator.focus();
83
+ }
84
+ }
@@ -0,0 +1,84 @@
1
+ import { snackbarControlTemplate } from '@ngneers/controls-themes/templates/snackbar';
2
+ import { themeClasses } from '../utils/theme';
3
+ import { expect, type Locator, type Page } from '@playwright/test';
4
+
5
+ export class NgnSnackbarHarness {
6
+ public readonly classes = themeClasses(snackbarControlTemplate);
7
+
8
+ public readonly locator: Locator;
9
+ public readonly header: Locator;
10
+ public readonly content: Locator;
11
+ public readonly icon: Locator;
12
+ public readonly closeButton: Locator;
13
+
14
+ constructor(locator: Locator) {
15
+ this.locator = locator;
16
+ this.header = locator.locator(this.classes.defaultHeader);
17
+ this.content = locator.locator(this.classes.defaultContent);
18
+ this.icon = this.header.locator('ngn-icon').first();
19
+ // The close button is a direct child of the snackbar root (sibling of the body), not inside
20
+ // the header. It is the only <button> in the snackbar, so match it directly.
21
+ this.closeButton = locator.locator('button');
22
+ }
23
+
24
+ public async expectVisible() {
25
+ await expect(this.locator).toBeVisible();
26
+ }
27
+
28
+ public async expectHidden() {
29
+ await expect(this.locator).not.toBeVisible();
30
+ }
31
+
32
+ public async expectHeader(text: string) {
33
+ await expect(this.header).toContainText(text);
34
+ }
35
+
36
+ public async expectContent(text: string) {
37
+ await expect(this.content).toContainText(text);
38
+ }
39
+
40
+ public async expectIcon(hasIcon: boolean) {
41
+ if (hasIcon) {
42
+ await expect(this.icon).toBeVisible();
43
+ } else {
44
+ await expect(this.icon).not.toBeVisible();
45
+ }
46
+ }
47
+
48
+ public async expectClosable(closable: boolean) {
49
+ if (closable) {
50
+ await expect(this.closeButton).toBeVisible();
51
+ } else {
52
+ await expect(this.closeButton).not.toBeVisible();
53
+ }
54
+ }
55
+
56
+ public async close() {
57
+ await this.closeButton.click();
58
+ }
59
+ }
60
+
61
+ export class NgnSnackbarHostHarness {
62
+ public readonly classes = themeClasses(snackbarControlTemplate);
63
+ public readonly locator: Locator;
64
+
65
+ constructor(page: Page) {
66
+ this.locator = page.locator('ngn-snackbar-host');
67
+ }
68
+
69
+ public getSnackbar(index: number = 0): NgnSnackbarHarness {
70
+ return new NgnSnackbarHarness(this.locator.locator('ngn-snackbar').nth(index));
71
+ }
72
+
73
+ public getAllSnackbars(): Locator {
74
+ return this.locator.locator('ngn-snackbar');
75
+ }
76
+
77
+ public async expectSnackbarCount(count: number) {
78
+ await expect(this.getAllSnackbars()).toHaveCount(count);
79
+ }
80
+
81
+ public async expectVisible() {
82
+ await expect(this.locator).toBeVisible();
83
+ }
84
+ }