@react-aria/test-utils 1.0.0-alpha.7 → 1.0.0-beta.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.
Files changed (64) hide show
  1. package/dist/combobox.main.js +5 -4
  2. package/dist/combobox.main.js.map +1 -1
  3. package/dist/combobox.mjs +5 -4
  4. package/dist/combobox.module.js +5 -4
  5. package/dist/combobox.module.js.map +1 -1
  6. package/dist/events.main.js +31 -1
  7. package/dist/events.main.js.map +1 -1
  8. package/dist/events.mjs +30 -2
  9. package/dist/events.module.js +30 -2
  10. package/dist/events.module.js.map +1 -1
  11. package/dist/gridlist.main.js +23 -9
  12. package/dist/gridlist.main.js.map +1 -1
  13. package/dist/gridlist.mjs +24 -10
  14. package/dist/gridlist.module.js +24 -10
  15. package/dist/gridlist.module.js.map +1 -1
  16. package/dist/listbox.main.js +22 -13
  17. package/dist/listbox.main.js.map +1 -1
  18. package/dist/listbox.mjs +23 -14
  19. package/dist/listbox.module.js +23 -14
  20. package/dist/listbox.module.js.map +1 -1
  21. package/dist/menu.main.js +1 -1
  22. package/dist/menu.main.js.map +1 -1
  23. package/dist/menu.mjs +1 -1
  24. package/dist/menu.module.js +1 -1
  25. package/dist/menu.module.js.map +1 -1
  26. package/dist/select.main.js +7 -4
  27. package/dist/select.main.js.map +1 -1
  28. package/dist/select.mjs +7 -4
  29. package/dist/select.module.js +7 -4
  30. package/dist/select.module.js.map +1 -1
  31. package/dist/table.main.js +42 -9
  32. package/dist/table.main.js.map +1 -1
  33. package/dist/table.mjs +44 -11
  34. package/dist/table.module.js +44 -11
  35. package/dist/table.module.js.map +1 -1
  36. package/dist/tabs.main.js +1 -1
  37. package/dist/tabs.main.js.map +1 -1
  38. package/dist/tabs.mjs +1 -1
  39. package/dist/tabs.module.js +1 -1
  40. package/dist/tabs.module.js.map +1 -1
  41. package/dist/tree.main.js +27 -8
  42. package/dist/tree.main.js.map +1 -1
  43. package/dist/tree.mjs +28 -9
  44. package/dist/tree.module.js +28 -9
  45. package/dist/tree.module.js.map +1 -1
  46. package/dist/types.d.ts +29 -9
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/user.main.js.map +1 -1
  49. package/dist/user.module.js.map +1 -1
  50. package/dist/userEventMaps.main.js.map +1 -1
  51. package/dist/userEventMaps.module.js.map +1 -1
  52. package/package.json +7 -3
  53. package/src/combobox.ts +10 -7
  54. package/src/events.ts +34 -2
  55. package/src/gridlist.ts +35 -12
  56. package/src/listbox.ts +47 -19
  57. package/src/menu.ts +1 -1
  58. package/src/select.ts +10 -4
  59. package/src/table.ts +71 -10
  60. package/src/tabs.ts +1 -1
  61. package/src/tree.ts +40 -10
  62. package/src/types.ts +11 -1
  63. package/src/user.ts +10 -1
  64. package/src/userEventMaps.ts +1 -1
@@ -1 +1 @@
1
- {"mappings":";;;;;;;;;;;AAAA;;;;;;;;;;CAUC;;;;;;;;;;AAwBD,IAAI,kCAAY;IACd,UAAU,CAAA,GAAA,yCAAW;IACrB,SAAS,CAAA,GAAA,wCAAU;IACnB,QAAQ,CAAA,GAAA,yCAAS;IACjB,YAAY,CAAA,GAAA,yCAAa;IACzB,YAAY,CAAA,GAAA,yCAAa;IACzB,WAAW,CAAA,GAAA,yCAAY;IACvB,QAAQ,CAAA,GAAA,yCAAS;IACjB,QAAQ,CAAA,GAAA,yCAAS;AACnB;AA0BA,IAAI,4CAAsB,CAAC,WAAiC,IAAI,QAAQ,CAAC,UAAY,WAAW,SAAS;AAElG,MAAM;IAoBX;;GAEC,GACD,aAAqC,WAAc,EAAE,IAAmB,EAAa;QACnF,OAAO,IAAI,AAAC,+BAAU,CAAC,YAAY,CAAC;YAAC,iBAAiB,IAAI,CAAC,eAAe;YAAE,cAAc,IAAI,CAAC,YAAY;YAAE,GAAG,IAAI;YAAE,MAAM,IAAI,CAAC,IAAI;QAAA;IACvI;IAZA,YAAY,OAAiB,CAAC,CAAC,CAAE;QAC/B,IAAI,mBAAC,eAAe,gBAAE,YAAY,EAAC,GAAG;QACtC,IAAI,CAAC,IAAI,GAAG,CAAA,GAAA,8BAAQ,EAAE,KAAK,CAAC;YAAC,OAAO;wBAAM;QAAU;QACpD,IAAI,CAAC,eAAe,GAAG;QACvB,IAAI,CAAC,YAAY,GAAG,gBAAgB;IACtC;AAQF","sources":["packages/@react-aria/test-utils/src/user.ts"],"sourcesContent":["/*\n * Copyright 2024 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {ComboBoxTester} from './combobox';\nimport {\n ComboBoxTesterOpts,\n GridListTesterOpts,\n ListBoxTesterOpts,\n MenuTesterOpts,\n SelectTesterOpts,\n TableTesterOpts,\n TabsTesterOpts,\n TreeTesterOpts,\n UserOpts\n} from './types';\nimport {GridListTester} from './gridlist';\nimport {ListBoxTester} from './listbox';\nimport {MenuTester} from './menu';\nimport {pointerMap} from './';\nimport {SelectTester} from './select';\nimport {TableTester} from './table';\nimport {TabsTester} from './tabs';\nimport {TreeTester} from './tree';\nimport userEvent from '@testing-library/user-event';\n\nlet keyToUtil = {\n 'Select': SelectTester,\n 'Table': TableTester,\n 'Menu': MenuTester,\n 'ComboBox': ComboBoxTester,\n 'GridList': GridListTester,\n 'ListBox': ListBoxTester,\n 'Tabs': TabsTester,\n 'Tree': TreeTester\n} as const;\nexport type PatternNames = keyof typeof keyToUtil;\n\n// Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html\ntype Tester<T> =\n T extends 'ComboBox' ? ComboBoxTester :\n T extends 'GridList' ? GridListTester :\n T extends 'ListBox' ? ListBoxTester :\n T extends 'Menu' ? MenuTester :\n T extends 'Select' ? SelectTester :\n T extends 'Table' ? TableTester :\n T extends 'Tabs' ? TabsTester :\n T extends 'Tree' ? TreeTester :\n never;\n\ntype TesterOpts<T> =\n T extends 'ComboBox' ? ComboBoxTesterOpts :\n T extends 'GridList' ? GridListTesterOpts :\n T extends 'ListBox' ? ListBoxTesterOpts :\n T extends 'Menu' ? MenuTesterOpts :\n T extends 'Select' ? SelectTesterOpts :\n T extends 'Table' ? TableTesterOpts :\n T extends 'Tabs' ? TabsTesterOpts :\n T extends 'Tree' ? TreeTesterOpts :\n never;\n\nlet defaultAdvanceTimer = (waitTime: number | undefined) => new Promise((resolve) => setTimeout(resolve, waitTime));\n\nexport class User {\n private user;\n /**\n * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden\n * at the aria pattern util level if needed.\n * @default mouse\n */\n interactionType: UserOpts['interactionType'];\n /**\n * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table).\n */\n advanceTimer: UserOpts['advanceTimer'];\n\n constructor(opts: UserOpts = {}) {\n let {interactionType, advanceTimer} = opts;\n this.user = userEvent.setup({delay: null, pointerMap});\n this.interactionType = interactionType;\n this.advanceTimer = advanceTimer || defaultAdvanceTimer;\n }\n\n /**\n * Creates an aria pattern tester, inheriting the options provided to the original user.\n */\n createTester<T extends PatternNames>(patternName: T, opts: TesterOpts<T>): Tester<T> {\n return new (keyToUtil)[patternName]({interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts, user: this.user}) as Tester<T>;\n }\n}\n"],"names":[],"version":3,"file":"user.module.js.map"}
1
+ {"mappings":";;;;;;;;;;;AAAA;;;;;;;;;;CAUC;;;;;;;;;;AAwBD,IAAI,kCASA;IACF,UAAU,CAAA,GAAA,yCAAW;IACrB,SAAS,CAAA,GAAA,wCAAU;IACnB,QAAQ,CAAA,GAAA,yCAAS;IACjB,YAAY,CAAA,GAAA,yCAAa;IACzB,YAAY,CAAA,GAAA,yCAAa;IACzB,WAAW,CAAA,GAAA,yCAAY;IACvB,QAAQ,CAAA,GAAA,yCAAS;IACjB,QAAQ,CAAA,GAAA,yCAAS;AACnB;AA0BA,IAAI,4CAAsB,CAAC,WAAiC,IAAI,QAAQ,CAAC,UAAY,WAAW,SAAS;AAElG,MAAM;IAoBX;;GAEC,GACD,aAAqC,WAAc,EAAE,IAAmB,EAAa;QACnF,OAAO,IAAI,AAAC,+BAAU,CAAC,YAAY,CAAC;YAAC,iBAAiB,IAAI,CAAC,eAAe;YAAE,cAAc,IAAI,CAAC,YAAY;YAAE,GAAG,IAAI;YAAE,MAAM,IAAI,CAAC,IAAI;QAAA;IACvI;IAZA,YAAY,OAAiB,CAAC,CAAC,CAAE;QAC/B,IAAI,mBAAC,eAAe,gBAAE,YAAY,EAAC,GAAG;QACtC,IAAI,CAAC,IAAI,GAAG,CAAA,GAAA,8BAAQ,EAAE,KAAK,CAAC;YAAC,OAAO;wBAAM;QAAU;QACpD,IAAI,CAAC,eAAe,GAAG;QACvB,IAAI,CAAC,YAAY,GAAG,gBAAgB;IACtC;AAQF","sources":["packages/@react-aria/test-utils/src/user.ts"],"sourcesContent":["/*\n * Copyright 2024 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {ComboBoxTester} from './combobox';\nimport {\n ComboBoxTesterOpts,\n GridListTesterOpts,\n ListBoxTesterOpts,\n MenuTesterOpts,\n SelectTesterOpts,\n TableTesterOpts,\n TabsTesterOpts,\n TreeTesterOpts,\n UserOpts\n} from './types';\nimport {GridListTester} from './gridlist';\nimport {ListBoxTester} from './listbox';\nimport {MenuTester} from './menu';\nimport {pointerMap} from './';\nimport {SelectTester} from './select';\nimport {TableTester} from './table';\nimport {TabsTester} from './tabs';\nimport {TreeTester} from './tree';\nimport userEvent from '@testing-library/user-event';\n\nlet keyToUtil: {\n 'Select': typeof SelectTester,\n 'Table': typeof TableTester,\n 'Menu': typeof MenuTester,\n 'ComboBox': typeof ComboBoxTester,\n 'GridList': typeof GridListTester,\n 'ListBox': typeof ListBoxTester,\n 'Tabs': typeof TabsTester,\n 'Tree': typeof TreeTester\n} = {\n 'Select': SelectTester,\n 'Table': TableTester,\n 'Menu': MenuTester,\n 'ComboBox': ComboBoxTester,\n 'GridList': GridListTester,\n 'ListBox': ListBoxTester,\n 'Tabs': TabsTester,\n 'Tree': TreeTester\n} as const;\nexport type PatternNames = keyof typeof keyToUtil;\n\n// Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html\ntype Tester<T> =\n T extends 'ComboBox' ? ComboBoxTester :\n T extends 'GridList' ? GridListTester :\n T extends 'ListBox' ? ListBoxTester :\n T extends 'Menu' ? MenuTester :\n T extends 'Select' ? SelectTester :\n T extends 'Table' ? TableTester :\n T extends 'Tabs' ? TabsTester :\n T extends 'Tree' ? TreeTester :\n never;\n\ntype TesterOpts<T> =\n T extends 'ComboBox' ? ComboBoxTesterOpts :\n T extends 'GridList' ? GridListTesterOpts :\n T extends 'ListBox' ? ListBoxTesterOpts :\n T extends 'Menu' ? MenuTesterOpts :\n T extends 'Select' ? SelectTesterOpts :\n T extends 'Table' ? TableTesterOpts :\n T extends 'Tabs' ? TabsTesterOpts :\n T extends 'Tree' ? TreeTesterOpts :\n never;\n\nlet defaultAdvanceTimer = (waitTime: number | undefined) => new Promise((resolve) => setTimeout(resolve, waitTime));\n\nexport class User {\n private user;\n /**\n * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden\n * at the aria pattern util level if needed.\n * @default mouse\n */\n interactionType: UserOpts['interactionType'];\n /**\n * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table).\n */\n advanceTimer: UserOpts['advanceTimer'];\n\n constructor(opts: UserOpts = {}) {\n let {interactionType, advanceTimer} = opts;\n this.user = userEvent.setup({delay: null, pointerMap});\n this.interactionType = interactionType;\n this.advanceTimer = advanceTimer || defaultAdvanceTimer;\n }\n\n /**\n * Creates an aria pattern tester, inheriting the options provided to the original user.\n */\n createTester<T extends PatternNames>(patternName: T, opts: TesterOpts<T>): Tester<T> {\n return new (keyToUtil)[patternName]({interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts, user: this.user}) as Tester<T>;\n }\n}\n"],"names":[],"version":3,"file":"user.module.js.map"}
@@ -1 +1 @@
1
- {"mappings":";;;;;;AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event/system/pointer/shared';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.main.js.map"}
1
+ {"mappings":";;;;;;AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.main.js.map"}
@@ -1 +1 @@
1
- {"mappings":"AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event/system/pointer/shared';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.module.js.map"}
1
+ {"mappings":"AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.module.js.map"}
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@react-aria/test-utils",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-beta.0",
4
4
  "description": "Testing utils for react-aria patterns",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
7
7
  "module": "dist/module.js",
8
8
  "exports": {
9
- "types": "./dist/types.d.ts",
9
+ "source": "./src/index.ts",
10
+ "types": [
11
+ "./dist/types.d.ts",
12
+ "./src/index.ts"
13
+ ],
10
14
  "import": "./dist/import.mjs",
11
15
  "require": "./dist/main.js"
12
16
  },
@@ -33,5 +37,5 @@
33
37
  "publishConfig": {
34
38
  "access": "public"
35
39
  },
36
- "gitHead": "9c77d4e8267ed39469c65f65da94ece7be509874"
40
+ "gitHead": "8b9348ff255e018b2dd9b27e2a45507cadfa1d35"
37
41
  }
package/src/combobox.ts CHANGED
@@ -55,14 +55,17 @@ export class ComboBoxTester {
55
55
  if (trigger) {
56
56
  this._trigger = trigger;
57
57
  } else {
58
- let trigger = within(root).queryByRole('button', {hidden: true});
59
- if (trigger) {
60
- this._trigger = trigger;
61
- } else {
62
- // For cases like https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ where the combobox
63
- // is also the trigger button
64
- this._trigger = this._combobox;
58
+ let buttons = within(root).queryAllByRole('button', {hidden: true});
59
+
60
+ if (buttons.length === 1) {
61
+ trigger = buttons[0];
62
+ } else if (buttons.length > 1) {
63
+ trigger = buttons.find(button => button.hasAttribute('aria-haspopup'));
65
64
  }
65
+
66
+ // For cases like https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ where the combobox
67
+ // is also the trigger button
68
+ this._trigger = trigger || this._combobox;
66
69
  }
67
70
  }
68
71
 
package/src/events.ts CHANGED
@@ -14,6 +14,37 @@ import {act, fireEvent} from '@testing-library/react';
14
14
  import {UserOpts} from './types';
15
15
 
16
16
  export const DEFAULT_LONG_PRESS_TIME = 500;
17
+ function testPlatform(re: RegExp) {
18
+ return typeof window !== 'undefined' && window.navigator != null
19
+ ? re.test(window.navigator['userAgentData']?.platform || window.navigator.platform)
20
+ : false;
21
+ }
22
+
23
+ function cached(fn: () => boolean) {
24
+ if (process.env.NODE_ENV === 'test') {
25
+ return fn;
26
+ }
27
+
28
+ let res: boolean | null = null;
29
+ return () => {
30
+ if (res == null) {
31
+ res = fn();
32
+ }
33
+ return res;
34
+ };
35
+ }
36
+
37
+ const isMac = cached(function () {
38
+ return testPlatform(/^Mac/i);
39
+ });
40
+
41
+ export function getAltKey(): 'Alt' | 'ControlLeft' {
42
+ return isMac() ? 'Alt' : 'ControlLeft';
43
+ }
44
+
45
+ export function getMetaKey(): 'MetaLeft' | 'ControlLeft' {
46
+ return isMac() ? 'MetaLeft' : 'ControlLeft';
47
+ }
17
48
 
18
49
  /**
19
50
  * Simulates a "long press" event on a element.
@@ -58,9 +89,10 @@ export async function triggerLongPress(opts: {element: HTMLElement, advanceTimer
58
89
  }
59
90
 
60
91
  // Docs cannot handle the types that userEvent actually declares, so hopefully this sub set is okay
61
- export async function pressElement(user: {click: (element: Element) => Promise<void>, keyboard: (keys: string) => Promise<void>, pointer: (opts: {target: Element, keys: string}) => Promise<void>}, element: HTMLElement, interactionType: UserOpts['interactionType']): Promise<void> {
92
+ export async function pressElement(user: {click: (element: Element) => Promise<void>, keyboard: (keys: string) => Promise<void>, pointer: (opts: {target: Element, keys: string, coords?: any}) => Promise<void>}, element: HTMLElement, interactionType: UserOpts['interactionType']): Promise<void> {
62
93
  if (interactionType === 'mouse') {
63
- await user.click(element);
94
+ // Add coords with pressure so this isn't detected as a virtual click
95
+ await user.pointer({target: element, keys: '[MouseLeft]', coords: {pressure: .5}});
64
96
  } else if (interactionType === 'keyboard') {
65
97
  // TODO: For the keyboard flow, I wonder if it would be reasonable to just do fireEvent directly on the obtained row node or if we should
66
98
  // stick to simulting an actual user's keyboard operations as closely as possible
package/src/gridlist.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  */
12
12
 
13
13
  import {act, within} from '@testing-library/react';
14
+ import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
14
15
  import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types';
15
- import {pressElement, triggerLongPress} from './events';
16
16
 
17
17
  interface GridListToggleRowOpts extends ToggleGridRowOpts {}
18
18
  interface GridListRowActionOpts extends GridRowActionOpts {}
@@ -57,20 +57,21 @@ export class GridListTester {
57
57
  }
58
58
 
59
59
  // TODO: RTL
60
- private async keyboardNavigateToRow(opts: {row: HTMLElement}) {
61
- let {row} = opts;
60
+ private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
61
+ let {row, selectionOnNav = 'default'} = opts;
62
+ let altKey = getAltKey();
62
63
  let rows = this.rows;
63
64
  let targetIndex = rows.indexOf(row);
64
65
  if (targetIndex === -1) {
65
66
  throw new Error('Option provided is not in the gridlist');
66
67
  }
67
68
 
68
- if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) {
69
+ if (document.activeElement !== this._gridlist && !this._gridlist.contains(document.activeElement)) {
69
70
  act(() => this._gridlist.focus());
70
71
  }
71
72
 
72
73
  if (document.activeElement === this._gridlist) {
73
- await this.user.keyboard('[ArrowDown]');
74
+ await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
74
75
  } else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
75
76
  do {
76
77
  await this.user.keyboard('[ArrowLeft]');
@@ -82,22 +83,33 @@ export class GridListTester {
82
83
  }
83
84
  let direction = targetIndex > currIndex ? 'down' : 'up';
84
85
 
86
+ if (selectionOnNav === 'none') {
87
+ await this.user.keyboard(`[${altKey}>]`);
88
+ }
85
89
  for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
86
90
  await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
87
91
  }
92
+ if (selectionOnNav === 'none') {
93
+ await this.user.keyboard(`[/${altKey}]`);
94
+ }
88
95
  };
89
96
 
90
97
  /**
91
98
  * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester.
99
+ * Note that this will endevor to always add/remove JUST the provided row to the set of selected rows.
92
100
  */
93
101
  async toggleRowSelection(opts: GridListToggleRowOpts): Promise<void> {
94
102
  let {
95
103
  row,
96
104
  needsLongPress,
97
105
  checkboxSelection = true,
98
- interactionType = this._interactionType
106
+ interactionType = this._interactionType,
107
+ selectionBehavior = 'toggle'
99
108
  } = opts;
100
109
 
110
+ let altKey = getAltKey();
111
+ let metaKey = getMetaKey();
112
+
101
113
  if (typeof row === 'string' || typeof row === 'number') {
102
114
  row = this.findRow({rowIndexOrText: row});
103
115
  }
@@ -116,9 +128,15 @@ export class GridListTester {
116
128
 
117
129
  // this would be better than the check to do nothing in events.ts
118
130
  // also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly
119
- if (interactionType === 'keyboard' && !checkboxSelection) {
120
- await this.keyboardNavigateToRow({row});
121
- await this.user.keyboard('{Space}');
131
+ if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
132
+ await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
133
+ if (selectionBehavior === 'replace') {
134
+ await this.user.keyboard(`[${altKey}>]`);
135
+ }
136
+ await this.user.keyboard('[Space]');
137
+ if (selectionBehavior === 'replace') {
138
+ await this.user.keyboard(`[/${altKey}]`);
139
+ }
122
140
  return;
123
141
  }
124
142
  if (rowCheckbox && checkboxSelection) {
@@ -132,9 +150,14 @@ export class GridListTester {
132
150
 
133
151
  // Note that long press interactions with rows is strictly touch only for grid rows
134
152
  await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
135
-
136
153
  } else {
137
- await pressElement(this.user, cell, interactionType);
154
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
155
+ await this.user.keyboard(`[${metaKey}>]`);
156
+ }
157
+ await pressElement(this.user, row, interactionType);
158
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
159
+ await this.user.keyboard(`[/${metaKey}]`);
160
+ }
138
161
  }
139
162
  }
140
163
  }
@@ -166,7 +189,7 @@ export class GridListTester {
166
189
  return;
167
190
  }
168
191
 
169
- await this.keyboardNavigateToRow({row});
192
+ await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
170
193
  await this.user.keyboard('[Enter]');
171
194
  } else {
172
195
  await pressElement(this.user, row, interactionType);
package/src/listbox.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  */
12
12
 
13
13
  import {act, within} from '@testing-library/react';
14
+ import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
14
15
  import {ListBoxTesterOpts, UserOpts} from './types';
15
- import {pressElement, triggerLongPress} from './events';
16
16
 
17
17
  interface ListBoxToggleOptionOpts {
18
18
  /**
@@ -31,7 +31,16 @@ interface ListBoxToggleOptionOpts {
31
31
  /**
32
32
  * Whether the option needs to be long pressed to be selected. Depends on the listbox's implementation.
33
33
  */
34
- needsLongPress?: boolean
34
+ needsLongPress?: boolean,
35
+ /**
36
+ * Whether the listbox has a selectionBehavior of "toggle" or "replace" (aka highlight selection). This affects the user operations
37
+ * required to toggle option selection by adding modifier keys during user actions, useful when performing multi-option selection in a "selectionBehavior: 'replace'" listbox.
38
+ * If you would like to still simulate user actions (aka press) without these modifiers keys for a "selectionBehavior: replace" listbox, simply omit this option.
39
+ * See the [RAC Listbox docs](https://react-spectrum.adobe.com/react-aria/ListBox.html#selection-behavior) for more info on this behavior.
40
+ *
41
+ * @default 'toggle'
42
+ */
43
+ selectionBehavior?: 'toggle' | 'replace'
35
44
  }
36
45
 
37
46
  interface ListBoxOptionActionOpts extends Omit<ListBoxToggleOptionOpts, 'keyboardActivation' | 'needsLongPress'> {
@@ -85,44 +94,51 @@ export class ListBoxTester {
85
94
 
86
95
  // TODO: this is basically the same as menu except for the error message, refactor later so that they share
87
96
  // TODO: this also doesn't support grid layout yet
88
- private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
89
- let {option} = opts;
97
+ private async keyboardNavigateToOption(opts: {option: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
98
+ let {option, selectionOnNav = 'default'} = opts;
99
+ let altKey = getAltKey();
90
100
  let options = this.options();
91
101
  let targetIndex = options.indexOf(option);
92
102
  if (targetIndex === -1) {
93
103
  throw new Error('Option provided is not in the listbox');
94
104
  }
95
105
 
96
- if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
106
+ if (document.activeElement !== this._listbox && !this._listbox.contains(document.activeElement)) {
97
107
  act(() => this._listbox.focus());
98
- }
99
-
100
- await this.user.keyboard('[ArrowDown]');
101
-
102
- // TODO: not sure about doing same while loop that exists in other implementations of keyboardNavigateToOption,
103
- // feels like it could break easily
104
- if (document.activeElement?.getAttribute('role') !== 'option') {
105
- await act(async () => {
106
- option.focus();
107
- });
108
+ await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
108
109
  }
109
110
 
110
111
  let currIndex = options.indexOf(document.activeElement as HTMLElement);
111
112
  if (currIndex === -1) {
112
113
  throw new Error('ActiveElement is not in the listbox');
113
114
  }
114
- let direction = targetIndex > currIndex ? 'down' : 'up';
115
115
 
116
+ let direction = targetIndex > currIndex ? 'down' : 'up';
117
+ if (selectionOnNav === 'none') {
118
+ await this.user.keyboard(`[${altKey}>]`);
119
+ }
116
120
  for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
117
121
  await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
118
122
  }
123
+ if (selectionOnNav === 'none') {
124
+ await this.user.keyboard(`[/${altKey}]`);
125
+ }
119
126
  };
120
127
 
121
128
  /**
122
129
  * Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester.
123
130
  */
124
131
  async toggleOptionSelection(opts: ListBoxToggleOptionOpts): Promise<void> {
125
- let {option, needsLongPress, keyboardActivation = 'Enter', interactionType = this._interactionType} = opts;
132
+ let {
133
+ option,
134
+ needsLongPress,
135
+ keyboardActivation = 'Enter',
136
+ interactionType = this._interactionType,
137
+ selectionBehavior = 'toggle'
138
+ } = opts;
139
+
140
+ let altKey = getAltKey();
141
+ let metaKey = getMetaKey();
126
142
 
127
143
  if (typeof option === 'string' || typeof option === 'number') {
128
144
  option = this.findOption({optionIndexOrText: option});
@@ -137,8 +153,14 @@ export class ListBoxTester {
137
153
  return;
138
154
  }
139
155
 
140
- await this.keyboardNavigateToOption({option});
156
+ await this.keyboardNavigateToOption({option, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
157
+ if (selectionBehavior === 'replace') {
158
+ await this.user.keyboard(`[${altKey}>]`);
159
+ }
141
160
  await this.user.keyboard(`[${keyboardActivation}]`);
161
+ if (selectionBehavior === 'replace') {
162
+ await this.user.keyboard(`[/${altKey}]`);
163
+ }
142
164
  } else {
143
165
  if (needsLongPress && interactionType === 'touch') {
144
166
  if (this._advanceTimer == null) {
@@ -147,7 +169,13 @@ export class ListBoxTester {
147
169
 
148
170
  await triggerLongPress({element: option, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
149
171
  } else {
172
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
173
+ await this.user.keyboard(`[${metaKey}>]`);
174
+ }
150
175
  await pressElement(this.user, option, interactionType);
176
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
177
+ await this.user.keyboard(`[/${metaKey}]`);
178
+ }
151
179
  }
152
180
  }
153
181
  }
@@ -177,7 +205,7 @@ export class ListBoxTester {
177
205
  return;
178
206
  }
179
207
 
180
- await this.keyboardNavigateToOption({option});
208
+ await this.keyboardNavigateToOption({option, selectionOnNav: 'none'});
181
209
  await this.user.keyboard('[Enter]');
182
210
  } else {
183
211
  await pressElement(this.user, option, interactionType);
package/src/menu.ts CHANGED
@@ -215,7 +215,7 @@ export class MenuTester {
215
215
  return;
216
216
  }
217
217
 
218
- if (document.activeElement !== menu || !menu.contains(document.activeElement)) {
218
+ if (document.activeElement !== menu && !menu.contains(document.activeElement)) {
219
219
  act(() => menu.focus());
220
220
  }
221
221
 
package/src/select.ts CHANGED
@@ -37,11 +37,17 @@ export class SelectTester {
37
37
  this.user = user;
38
38
  this._interactionType = interactionType || 'mouse';
39
39
  // Handle case where the wrapper element is provided rather than the Select's button (aka RAC)
40
- let triggerButton = within(root).queryByRole('button');
41
- if (triggerButton == null) {
40
+ let buttons = within(root).queryAllByRole('button');
41
+ let triggerButton;
42
+ if (buttons.length === 0) {
42
43
  triggerButton = root;
44
+ } else if (buttons.length === 1) {
45
+ triggerButton = buttons[0];
46
+ } else {
47
+ triggerButton = buttons.find(button => button.hasAttribute('aria-haspopup'));
43
48
  }
44
- this._trigger = triggerButton;
49
+
50
+ this._trigger = triggerButton ?? root;
45
51
  }
46
52
  /**
47
53
  * Set the interaction type used by the select tester.
@@ -183,7 +189,7 @@ export class SelectTester {
183
189
  return;
184
190
  }
185
191
 
186
- if (document.activeElement !== listbox || !listbox.contains(document.activeElement)) {
192
+ if (document.activeElement !== listbox && !listbox.contains(document.activeElement)) {
187
193
  act(() => listbox.focus());
188
194
  }
189
195
  await this.keyboardNavigateToOption({option});
package/src/table.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  */
12
12
 
13
13
  import {act, waitFor, within} from '@testing-library/react';
14
+ import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
14
15
  import {GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types';
15
- import {pressElement, triggerLongPress} from './events';
16
16
 
17
17
  interface TableToggleRowOpts extends ToggleGridRowOpts {}
18
18
  interface TableToggleSortOpts {
@@ -54,6 +54,55 @@ export class TableTester {
54
54
  this._interactionType = type;
55
55
  }
56
56
 
57
+ // TODO: RTL
58
+ private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
59
+ let {row, selectionOnNav = 'default'} = opts;
60
+ let altKey = getAltKey();
61
+ let rows = this.rows;
62
+ let targetIndex = rows.indexOf(row);
63
+ if (targetIndex === -1) {
64
+ throw new Error('Row provided is not in the table');
65
+ }
66
+
67
+ // Move focus into the table
68
+ if (document.activeElement !== this._table && !this._table.contains(document.activeElement)) {
69
+ act(() => this._table.focus());
70
+ }
71
+
72
+ if (document.activeElement === this._table) {
73
+ await this.user.keyboard('[ArrowDown]');
74
+ }
75
+
76
+ // If focus is currently somewhere in the first row group (aka on a column), we want to keyboard navigate downwards till we reach the rows
77
+ if (this.rowGroups[0].contains(document.activeElement)) {
78
+ do {
79
+ await this.user.keyboard('[ArrowDown]');
80
+ } while (!this.rowGroups[1].contains(document.activeElement));
81
+ }
82
+
83
+ // Move focus onto the row itself
84
+ if (this.rowGroups[1].contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
85
+ do {
86
+ await this.user.keyboard('[ArrowLeft]');
87
+ } while (document.activeElement!.getAttribute('role') !== 'row');
88
+ }
89
+ let currIndex = rows.indexOf(document.activeElement as HTMLElement);
90
+ if (currIndex === -1) {
91
+ throw new Error('Current active element is not on any of the table rows');
92
+ }
93
+ let direction = targetIndex > currIndex ? 'down' : 'up';
94
+
95
+ if (selectionOnNav === 'none') {
96
+ await this.user.keyboard(`[${altKey}>]`);
97
+ }
98
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
99
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
100
+ }
101
+ if (selectionOnNav === 'none') {
102
+ await this.user.keyboard(`[/${altKey}]`);
103
+ }
104
+ };
105
+
57
106
  /**
58
107
  * Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester.
59
108
  */
@@ -62,9 +111,13 @@ export class TableTester {
62
111
  row,
63
112
  needsLongPress,
64
113
  checkboxSelection = true,
65
- interactionType = this._interactionType
114
+ interactionType = this._interactionType,
115
+ selectionBehavior = 'toggle'
66
116
  } = opts;
67
117
 
118
+ let altKey = getMetaKey();
119
+ let metaKey = getMetaKey();
120
+
68
121
  if (typeof row === 'string' || typeof row === 'number') {
69
122
  row = this.findRow({rowIndexOrText: row});
70
123
  }
@@ -75,12 +128,15 @@ export class TableTester {
75
128
 
76
129
  let rowCheckbox = within(row).queryByRole('checkbox');
77
130
 
78
- if (interactionType === 'keyboard' && !checkboxSelection) {
79
- // TODO: for now focus the row directly until I add keyboard navigation
80
- await act(async () => {
81
- row.focus();
82
- });
83
- await this.user.keyboard('{Space}');
131
+ if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
132
+ await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
133
+ if (selectionBehavior === 'replace') {
134
+ await this.user.keyboard(`[${altKey}>]`);
135
+ }
136
+ await this.user.keyboard('[Space]');
137
+ if (selectionBehavior === 'replace') {
138
+ await this.user.keyboard(`[/${altKey}]`);
139
+ }
84
140
  return;
85
141
  }
86
142
  if (rowCheckbox && checkboxSelection) {
@@ -95,7 +151,13 @@ export class TableTester {
95
151
  // Note that long press interactions with rows is strictly touch only for grid rows
96
152
  await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
97
153
  } else {
154
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
155
+ await this.user.keyboard(`[${metaKey}>]`);
156
+ }
98
157
  await pressElement(this.user, cell, interactionType);
158
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
159
+ await this.user.keyboard(`[/${metaKey}]`);
160
+ }
99
161
  }
100
162
  }
101
163
  };
@@ -297,8 +359,7 @@ export class TableTester {
297
359
  if (needsDoubleClick) {
298
360
  await this.user.dblClick(row);
299
361
  } else if (interactionType === 'keyboard') {
300
- // TODO: add keyboard navigation instead of focusing the row directly. Will need to consider if the focus in in the columns
301
- act(() => row.focus());
362
+ await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
302
363
  await this.user.keyboard('[Enter]');
303
364
  } else {
304
365
  await pressElement(this.user, row, interactionType);
package/src/tabs.ts CHANGED
@@ -137,7 +137,7 @@ export class TabsTester {
137
137
  }
138
138
 
139
139
  if (interactionType === 'keyboard') {
140
- if (document.activeElement !== this._tablist || !this._tablist.contains(document.activeElement)) {
140
+ if (document.activeElement !== this._tablist && !this._tablist.contains(document.activeElement)) {
141
141
  act(() => this._tablist.focus());
142
142
  }
143
143