You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

testSelector.ts 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import {
  21. BoundFunction,
  22. GetByBoundAttribute,
  23. GetByRole,
  24. GetByText,
  25. screen,
  26. waitForOptions,
  27. within,
  28. } from '@testing-library/react';
  29. function maybeScreen(container?: HTMLElement) {
  30. return container ? within(container) : screen;
  31. }
  32. interface ReactTestingQuery {
  33. find<T extends HTMLElement = HTMLElement>(
  34. container?: HTMLElement,
  35. waitForOptions?: waitForOptions,
  36. ): Promise<T>;
  37. findAll<T extends HTMLElement = HTMLElement>(
  38. container?: HTMLElement,
  39. waitForOptions?: waitForOptions,
  40. ): Promise<T[]>;
  41. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T;
  42. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T[];
  43. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T | null;
  44. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T[] | null;
  45. }
  46. export class QuerySelector {
  47. dispatchQuery: ReactTestingQuery;
  48. constructor(dispatchQuery: ReactTestingQuery) {
  49. this.dispatchQuery = dispatchQuery;
  50. }
  51. find<T extends HTMLElement = HTMLElement>(
  52. container?: HTMLElement,
  53. waitForOptions?: waitForOptions,
  54. ): Promise<T> {
  55. return this.dispatchQuery.find<T>(container, waitForOptions);
  56. }
  57. findAll<T extends HTMLElement = HTMLElement>(
  58. container?: HTMLElement,
  59. waitForOptions?: waitForOptions,
  60. ): Promise<T[]> {
  61. return this.dispatchQuery.findAll<T>(container, waitForOptions);
  62. }
  63. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T {
  64. return this.dispatchQuery.get<T>(container);
  65. }
  66. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T[] {
  67. return this.dispatchQuery.getAll<T>(container);
  68. }
  69. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T | null {
  70. return this.dispatchQuery.query<T>(container);
  71. }
  72. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T[] | null {
  73. return this.dispatchQuery.queryAll<T>(container);
  74. }
  75. getAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T {
  76. return this.getAll<T>(container)[index];
  77. }
  78. async findAt<T extends HTMLElement = HTMLElement>(
  79. index: number,
  80. container?: HTMLElement,
  81. waitForOptions?: waitForOptions,
  82. ): Promise<T> {
  83. return (await this.findAll<T>(container, waitForOptions))[index];
  84. }
  85. queryAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T | null {
  86. const all = this.queryAll<T>(container);
  87. if (all) {
  88. return all[index];
  89. }
  90. return null;
  91. }
  92. by(selector: ReactTestingQuery): QuerySelector {
  93. return new ChainDispatch(this, selector);
  94. }
  95. byText(...args: Parameters<BoundFunction<GetByText>>): QuerySelector {
  96. return this.by(new DispatchByText(args));
  97. }
  98. byRole(...args: Parameters<BoundFunction<GetByRole>>): QuerySelector {
  99. return this.by(new DispatchByRole(args));
  100. }
  101. byPlaceholderText(...args: Parameters<BoundFunction<GetByBoundAttribute>>): QuerySelector {
  102. return this.by(new DispatchByPlaceholderText(args));
  103. }
  104. byLabelText(...args: Parameters<BoundFunction<GetByText>>): QuerySelector {
  105. return this.by(new DispatchByLabelText(args));
  106. }
  107. byTestId(...args: Parameters<BoundFunction<GetByBoundAttribute>>): QuerySelector {
  108. return this.by(new DispatchByTestId(args));
  109. }
  110. byDisplayValue(...args: Parameters<BoundFunction<GetByBoundAttribute>>): QuerySelector {
  111. return this.by(new DispatchByDisplayValue(args));
  112. }
  113. byTitle(...args: Parameters<BoundFunction<GetByBoundAttribute>>): ReactTestingQuery {
  114. return this.by(new DispatchByTitle(args));
  115. }
  116. }
  117. class ChainDispatch extends QuerySelector {
  118. innerQuery: QuerySelector;
  119. constructor(insideQuery: QuerySelector, elementQuery: ReactTestingQuery) {
  120. super(elementQuery);
  121. this.innerQuery = insideQuery;
  122. }
  123. async find<T extends HTMLElement = HTMLElement>(
  124. container?: HTMLElement,
  125. waitForOptions?: waitForOptions,
  126. ) {
  127. let inside: HTMLElement;
  128. try {
  129. inside = await this.innerQuery.find(container, waitForOptions);
  130. } catch (e) {
  131. const elements = this.innerQuery.getAll(container);
  132. const all = (
  133. await Promise.all(
  134. elements.map((e) => this.dispatchQuery.findAll<T>(e, waitForOptions).catch(() => null)),
  135. )
  136. )
  137. .flat()
  138. .filter((e) => e !== null);
  139. if (all.length !== 1) {
  140. throw e;
  141. }
  142. return all[0] as T;
  143. }
  144. return this.dispatchQuery.find<T>(inside, waitForOptions);
  145. }
  146. async findAll<T extends HTMLElement = HTMLElement>(
  147. container?: HTMLElement,
  148. waitForOptions?: waitForOptions,
  149. ) {
  150. return this.dispatchQuery.findAll<T>(await this.innerQuery.find(container, waitForOptions));
  151. }
  152. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  153. let inside: HTMLElement;
  154. try {
  155. inside = this.innerQuery.get(container);
  156. } catch (e) {
  157. const elements = this.innerQuery.getAll(container);
  158. const all = elements.map((e) => this.dispatchQuery.query<T>(e)).filter((e) => e !== null);
  159. if (all.length !== 1) {
  160. throw e;
  161. }
  162. return all[0] as T;
  163. }
  164. return this.dispatchQuery.get<T>(inside);
  165. }
  166. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  167. const containers = this.innerQuery.getAll(container);
  168. return containers.reduce(
  169. (acc, item) => [...acc, ...(this.dispatchQuery.queryAll<T>(item) ?? [])],
  170. [],
  171. );
  172. }
  173. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  174. let inside: HTMLElement | null;
  175. try {
  176. inside = this.innerQuery.query(container);
  177. } catch (e) {
  178. const elements = this.innerQuery.queryAll(container);
  179. const all = elements?.map((e) => this.dispatchQuery.query<T>(e)).filter((e) => e !== null);
  180. if (all?.length !== 1) {
  181. throw e;
  182. }
  183. return all[0];
  184. }
  185. if (inside) {
  186. return this.dispatchQuery.query<T>(inside);
  187. }
  188. return null;
  189. }
  190. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  191. const innerContainer = this.innerQuery.query(container);
  192. if (innerContainer) {
  193. return this.dispatchQuery.queryAll<T>(innerContainer);
  194. }
  195. return null;
  196. }
  197. }
  198. class DispatchByText implements ReactTestingQuery {
  199. readonly args: Parameters<BoundFunction<GetByText>>;
  200. constructor(args: Parameters<BoundFunction<GetByText>>) {
  201. this.args = args;
  202. }
  203. find<T extends HTMLElement = HTMLElement>(
  204. container?: HTMLElement,
  205. waitForOptions?: waitForOptions,
  206. ) {
  207. return maybeScreen(container).findByText<T>(...this.args, waitForOptions);
  208. }
  209. findAll<T extends HTMLElement = HTMLElement>(
  210. container?: HTMLElement,
  211. waitForOptions?: waitForOptions,
  212. ) {
  213. return maybeScreen(container).findAllByText<T>(...this.args, waitForOptions);
  214. }
  215. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  216. return maybeScreen(container).getByText<T>(...this.args);
  217. }
  218. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  219. return maybeScreen(container).getAllByText<T>(...this.args);
  220. }
  221. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  222. return maybeScreen(container).queryByText<T>(...this.args);
  223. }
  224. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  225. return maybeScreen(container).queryAllByText<T>(...this.args);
  226. }
  227. }
  228. class DispatchByLabelText implements ReactTestingQuery {
  229. readonly args: Parameters<BoundFunction<GetByText>>;
  230. constructor(args: Parameters<BoundFunction<GetByText>>) {
  231. this.args = args;
  232. }
  233. find<T extends HTMLElement = HTMLElement>(
  234. container?: HTMLElement,
  235. waitForOptions?: waitForOptions,
  236. ) {
  237. return maybeScreen(container).findByLabelText<T>(...this.args, waitForOptions);
  238. }
  239. findAll<T extends HTMLElement = HTMLElement>(
  240. container?: HTMLElement,
  241. waitForOptions?: waitForOptions,
  242. ) {
  243. return maybeScreen(container).findAllByLabelText<T>(...this.args, waitForOptions);
  244. }
  245. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  246. return maybeScreen(container).getByLabelText<T>(...this.args);
  247. }
  248. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  249. return maybeScreen(container).getAllByLabelText<T>(...this.args);
  250. }
  251. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  252. return maybeScreen(container).queryByLabelText<T>(...this.args);
  253. }
  254. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  255. return maybeScreen(container).queryAllByLabelText<T>(...this.args);
  256. }
  257. }
  258. class DispatchByRole implements ReactTestingQuery {
  259. readonly args: Parameters<BoundFunction<GetByRole>>;
  260. constructor(args: Parameters<BoundFunction<GetByRole>>) {
  261. this.args = args;
  262. }
  263. find<T extends HTMLElement = HTMLElement>(
  264. container?: HTMLElement,
  265. waitForOptions?: waitForOptions,
  266. ) {
  267. return maybeScreen(container).findByRole<T>(...this.args, waitForOptions);
  268. }
  269. findAll<T extends HTMLElement = HTMLElement>(
  270. container?: HTMLElement,
  271. waitForOptions?: waitForOptions,
  272. ) {
  273. return maybeScreen(container).findAllByRole<T>(...this.args, waitForOptions);
  274. }
  275. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  276. return maybeScreen(container).getByRole<T>(...this.args);
  277. }
  278. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  279. return maybeScreen(container).getAllByRole<T>(...this.args);
  280. }
  281. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  282. return maybeScreen(container).queryByRole<T>(...this.args);
  283. }
  284. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  285. return maybeScreen(container).queryAllByRole<T>(...this.args);
  286. }
  287. }
  288. class DispatchByTestId implements ReactTestingQuery {
  289. readonly args: Parameters<BoundFunction<GetByBoundAttribute>>;
  290. constructor(args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  291. this.args = args;
  292. }
  293. find<T extends HTMLElement = HTMLElement>(
  294. container?: HTMLElement,
  295. waitForOptions?: waitForOptions,
  296. ) {
  297. return maybeScreen(container).findByTestId<T>(...this.args, waitForOptions);
  298. }
  299. findAll<T extends HTMLElement = HTMLElement>(
  300. container?: HTMLElement,
  301. waitForOptions?: waitForOptions,
  302. ) {
  303. return maybeScreen(container).findAllByTestId<T>(...this.args, waitForOptions);
  304. }
  305. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  306. return maybeScreen(container).getByTestId<T>(...this.args);
  307. }
  308. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  309. return maybeScreen(container).getAllByTestId<T>(...this.args);
  310. }
  311. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  312. return maybeScreen(container).queryByTestId<T>(...this.args);
  313. }
  314. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  315. return maybeScreen(container).queryAllByTestId<T>(...this.args);
  316. }
  317. }
  318. class DispatchByTitle implements ReactTestingQuery {
  319. readonly args: Parameters<BoundFunction<GetByBoundAttribute>>;
  320. constructor(args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  321. this.args = args;
  322. }
  323. find<T extends HTMLElement = HTMLElement>(
  324. container?: HTMLElement,
  325. waitForOptions?: waitForOptions,
  326. ) {
  327. return maybeScreen(container).findByTitle<T>(...this.args, waitForOptions);
  328. }
  329. findAll<T extends HTMLElement = HTMLElement>(
  330. container?: HTMLElement,
  331. waitForOptions?: waitForOptions,
  332. ) {
  333. return maybeScreen(container).findAllByTitle<T>(...this.args, waitForOptions);
  334. }
  335. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  336. return maybeScreen(container).getByTitle<T>(...this.args);
  337. }
  338. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  339. return maybeScreen(container).getAllByTitle<T>(...this.args);
  340. }
  341. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  342. return maybeScreen(container).queryByTitle<T>(...this.args);
  343. }
  344. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  345. return maybeScreen(container).queryAllByTitle<T>(...this.args);
  346. }
  347. }
  348. class DispatchByDisplayValue implements ReactTestingQuery {
  349. readonly args: Parameters<BoundFunction<GetByBoundAttribute>>;
  350. constructor(args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  351. this.args = args;
  352. }
  353. find<T extends HTMLElement = HTMLElement>(
  354. container?: HTMLElement,
  355. waitForOptions?: waitForOptions,
  356. ) {
  357. return maybeScreen(container).findByDisplayValue<T>(...this.args, waitForOptions);
  358. }
  359. findAll<T extends HTMLElement = HTMLElement>(
  360. container?: HTMLElement,
  361. waitForOptions?: waitForOptions,
  362. ) {
  363. return maybeScreen(container).findAllByDisplayValue<T>(...this.args, waitForOptions);
  364. }
  365. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  366. return maybeScreen(container).getByDisplayValue<T>(...this.args);
  367. }
  368. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  369. return maybeScreen(container).getAllByDisplayValue<T>(...this.args);
  370. }
  371. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  372. return maybeScreen(container).queryByDisplayValue<T>(...this.args);
  373. }
  374. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  375. return maybeScreen(container).queryAllByDisplayValue<T>(...this.args);
  376. }
  377. }
  378. class DispatchByPlaceholderText implements ReactTestingQuery {
  379. readonly args: Parameters<BoundFunction<GetByBoundAttribute>>;
  380. constructor(args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  381. this.args = args;
  382. }
  383. find<T extends HTMLElement = HTMLElement>(
  384. container?: HTMLElement,
  385. waitForOptions?: waitForOptions,
  386. ) {
  387. return maybeScreen(container).findByPlaceholderText<T>(...this.args, waitForOptions);
  388. }
  389. findAll<T extends HTMLElement = HTMLElement>(
  390. container?: HTMLElement,
  391. waitForOptions?: waitForOptions,
  392. ) {
  393. return maybeScreen(container).findAllByPlaceholderText<T>(...this.args, waitForOptions);
  394. }
  395. get<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  396. return maybeScreen(container).getByPlaceholderText<T>(...this.args);
  397. }
  398. getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  399. return maybeScreen(container).getAllByPlaceholderText<T>(...this.args);
  400. }
  401. query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  402. return maybeScreen(container).queryByPlaceholderText<T>(...this.args);
  403. }
  404. queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
  405. return maybeScreen(container).queryAllByPlaceholderText<T>(...this.args);
  406. }
  407. }
  408. export function byText(...args: Parameters<BoundFunction<GetByText>>) {
  409. return new QuerySelector(new DispatchByText(args));
  410. }
  411. export function byRole(...args: Parameters<BoundFunction<GetByRole>>) {
  412. return new QuerySelector(new DispatchByRole(args));
  413. }
  414. export function byPlaceholderText(...args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  415. return new QuerySelector(new DispatchByPlaceholderText(args));
  416. }
  417. export function byLabelText(...args: Parameters<BoundFunction<GetByText>>) {
  418. return new QuerySelector(new DispatchByLabelText(args));
  419. }
  420. export function byTestId(...args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  421. return new QuerySelector(new DispatchByTestId(args));
  422. }
  423. export function byTitle(...args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  424. return new QuerySelector(new DispatchByTitle(args));
  425. }
  426. export function byDisplayValue(...args: Parameters<BoundFunction<GetByBoundAttribute>>) {
  427. return new QuerySelector(new DispatchByDisplayValue(args));
  428. }