@rvoh/psychic-spec-helpers 0.7.0 → 0.7.2

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.
@@ -11,5 +11,17 @@ describe('check', () => {
11
11
  await check('not found checkbox', { timeout: 500 });
12
12
  }).rejects.toThrow();
13
13
  });
14
+ it('fails when the label points to an invalid input', async () => {
15
+ await check('My checkbox', { timeout: 500 });
16
+ await expect(async () => {
17
+ await check('My invalid label pointer checkbox', { timeout: 500 });
18
+ }).rejects.toThrow();
19
+ });
20
+ it('fails when the label points to a label with a missing htmlFor statement', async () => {
21
+ await check('My checkbox', { timeout: 500 });
22
+ await expect(async () => {
23
+ await check('My missing htmlFor checkbox', { timeout: 500 });
24
+ }).rejects.toThrow();
25
+ });
14
26
  });
15
27
  export {};
@@ -9,6 +9,19 @@ describe('uncheck', () => {
9
9
  inputValue = await page.$eval('#my-checkbox', input => input.checked).catch(() => null);
10
10
  expect(inputValue).toBe(false);
11
11
  });
12
+ it('succeeds when label has nested elements with text matching', async () => {
13
+ await check('My other checkbox');
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
15
+ let inputValue = await page
16
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
17
+ .$eval('#my-other-checkbox', input => input.checked)
18
+ .catch(() => null);
19
+ expect(inputValue).toBe(true);
20
+ await uncheck('My other checkbox');
21
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
22
+ inputValue = await page.$eval('#my-other-checkbox', input => input.checked).catch(() => null);
23
+ expect(inputValue).toBe(false);
24
+ });
12
25
  it('fails when the selector is not found', async () => {
13
26
  await check('My checkbox');
14
27
  await uncheck('My checkbox', { timeout: 500 });
@@ -16,5 +29,15 @@ describe('uncheck', () => {
16
29
  await uncheck('not found checkbox', { timeout: 500 });
17
30
  }).rejects.toThrow();
18
31
  });
32
+ it('fails when the selector is found, but it is not already checked', async () => {
33
+ await check('My checkbox');
34
+ await uncheck('My checkbox', { timeout: 500 });
35
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
36
+ const inputValue = await page.$eval('#my-checkbox', input => input.checked).catch(() => null);
37
+ expect(inputValue).toBe(false);
38
+ await expect(async () => {
39
+ await uncheck('My checkbox', { timeout: 500 });
40
+ }).rejects.toThrow();
41
+ });
19
42
  });
20
43
  export {};
@@ -0,0 +1,12 @@
1
+ export default function emptyForAttribute() {
2
+ return `
3
+ When using our uncheck helpers, we expect you to have the correct semantic layout for a checkbox on your page.
4
+ this includes the following:
5
+
6
+ 1.) a label element with matching text, with a "for" attribute set (or htmlFor, when using react)
7
+ 2.) an input with type="checkbox" which has an id that matches the corresponding for tag on the label
8
+
9
+ We were able to find a label matching the text you gave us, but the for attribute found on it
10
+ was blank. Make sure to add a for attribute, and point it to the id of your corresponding checkbox
11
+ `;
12
+ }
@@ -0,0 +1,10 @@
1
+ export default function missingForAttribute(forAttributeValue) {
2
+ return `
3
+ Expected to find an input element matching the "for" attribute on your label, but we could not locate that input element.
4
+
5
+ for attribute found: "${forAttributeValue}"
6
+
7
+ Make sure you have an input element with an id matching "${forAttributeValue}", otherwise this label
8
+ will not be able to be properly associated with the corresponding input.
9
+ `;
10
+ }
@@ -0,0 +1,26 @@
1
+ export default async function captureAttributeFromClosestElementWithType(cssSelector, type, attribute) {
2
+ const el = await page.waitForSelector(cssSelector, { timeout: 3000 });
3
+ const attributeValue = (await el.evaluate((element, type, attribute) => {
4
+ // Traverse up the DOM tree to find the closest parent label element
5
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
6
+ let currentElement = element;
7
+ let attrVal = null;
8
+ let millisDrift = 0;
9
+ const startMillis = Date.now();
10
+ while (!attrVal && currentElement && millisDrift < 3000) {
11
+ const currMillis = Date.now();
12
+ millisDrift = currMillis - startMillis;
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
+ if (currentElement.tagName === type.toUpperCase()) {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
16
+ attrVal = Array.from(currentElement.attributes).find(attr => attr.name === attribute)?.['value'];
17
+ }
18
+ else {
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
20
+ currentElement = currentElement.parentElement;
21
+ }
22
+ }
23
+ return attrVal;
24
+ }, type, attribute));
25
+ return attributeValue;
26
+ }
@@ -1,11 +1,31 @@
1
1
  import applyDefaultWaitForOpts from '../helpers/applyDefaultWaitForOpts.js';
2
+ import emptyForAttribute from '../error-messages/emptyForAttribute.js';
3
+ import missingInputToMatchForAttribute from '../error-messages/missingInputToMatchForAttribute.js';
4
+ import captureAttributeFromClosestElementWithType from '../internal/captureAttributeFromClosestElementWithType.js';
2
5
  export default async function toCheck(page, expectedText, opts) {
3
6
  const failure = {
4
7
  pass: false,
5
8
  message: () => `Expected page to have checkable element with text: "${expectedText}"`,
6
9
  };
10
+ const labelSelector = `label::-p-text("${expectedText}")`;
7
11
  try {
8
- await expect(page).toClickSelector(`label::-p-text("${expectedText}")`, applyDefaultWaitForOpts(opts));
12
+ const forAttributeValue = await captureAttributeFromClosestElementWithType(labelSelector, 'label', 'for');
13
+ if (!forAttributeValue) {
14
+ return {
15
+ pass: false,
16
+ message: () => emptyForAttribute(),
17
+ };
18
+ }
19
+ try {
20
+ await page.waitForSelector(`#${forAttributeValue}`, applyDefaultWaitForOpts(opts));
21
+ }
22
+ catch {
23
+ return {
24
+ pass: false,
25
+ message: () => missingInputToMatchForAttribute(forAttributeValue),
26
+ };
27
+ }
28
+ await expect(page).toClickSelector(labelSelector, applyDefaultWaitForOpts(opts));
9
29
  return {
10
30
  pass: true,
11
31
  message: () => {
@@ -13,7 +13,7 @@ export default async function toNotMatchTextContent(argumentPassedToExpect, expe
13
13
  successText: () => {
14
14
  throw new Error('Cannot negate toNotMatchTextContent, use toMatchTextContent instead');
15
15
  },
16
- failureText: r => `Expected ${r} to match text ${expected}`,
16
+ failureText: r => `Expected ${r} to not match text ${expected}, but it did`,
17
17
  timeout: opts.timeout,
18
18
  });
19
19
  }
@@ -1,10 +1,28 @@
1
1
  import applyDefaultWaitForOpts from '../helpers/applyDefaultWaitForOpts.js';
2
+ import emptyForAttribute from '../error-messages/emptyForAttribute.js';
3
+ import missingInputToMatchForAttribute from '../error-messages/missingInputToMatchForAttribute.js';
4
+ import captureAttributeFromClosestElementWithType from '../internal/captureAttributeFromClosestElementWithType.js';
2
5
  export default async function toUncheck(page, expectedText, opts) {
3
6
  try {
4
7
  const labelSelector = `label::-p-text("${expectedText}")`;
5
- // eslint-disable-next-line
6
- const forAttributeValue = await page.$eval(labelSelector, label => label.getAttribute('for'));
7
- const inputElement = await page.waitForSelector(`#${forAttributeValue}`, applyDefaultWaitForOpts(opts));
8
+ const forAttributeValue = await captureAttributeFromClosestElementWithType(labelSelector, 'label', 'for');
9
+ if (!forAttributeValue) {
10
+ return {
11
+ pass: false,
12
+ message: () => emptyForAttribute(),
13
+ };
14
+ }
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ let inputElement = null;
17
+ try {
18
+ inputElement = await page.waitForSelector(`#${forAttributeValue}`, applyDefaultWaitForOpts(opts));
19
+ }
20
+ catch {
21
+ return {
22
+ pass: false,
23
+ message: () => missingInputToMatchForAttribute(forAttributeValue),
24
+ };
25
+ }
8
26
  // eslint-disable-next-line
9
27
  const isChecked = await page.evaluate(checkbox => checkbox.checked, inputElement);
10
28
  if (!isChecked)
@@ -0,0 +1 @@
1
+ export default function emptyForAttribute(): string;
@@ -0,0 +1 @@
1
+ export default function missingForAttribute(forAttributeValue: string): string;
@@ -0,0 +1 @@
1
+ export default function captureAttributeFromClosestElementWithType(cssSelector: string, type: string, attribute: string): Promise<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic-spec-helpers",
4
- "version": "0.7.0",
4
+ "version": "0.7.2",
5
5
  "description": "psychic framework spec helpers",
6
6
  "author": "RVO Health",
7
7
  "repository": {
@@ -65,7 +65,7 @@
65
65
  "tsx": "^4.19.3",
66
66
  "typescript": "^5.5.4",
67
67
  "typescript-eslint": "^8.30.1",
68
- "vitest": "^3.1.1"
68
+ "vitest": "^3.1.3"
69
69
  },
70
70
  "dependencies": {
71
71
  "cookiejar": "^2.1.4"
@@ -0,0 +1,12 @@
1
+ export default function emptyForAttribute() {
2
+ return `
3
+ When using our uncheck helpers, we expect you to have the correct semantic layout for a checkbox on your page.
4
+ this includes the following:
5
+
6
+ 1.) a label element with matching text, with a "for" attribute set (or htmlFor, when using react)
7
+ 2.) an input with type="checkbox" which has an id that matches the corresponding for tag on the label
8
+
9
+ We were able to find a label matching the text you gave us, but the for attribute found on it
10
+ was blank. Make sure to add a for attribute, and point it to the id of your corresponding checkbox
11
+ `
12
+ }
@@ -0,0 +1,10 @@
1
+ export default function missingForAttribute(forAttributeValue: string) {
2
+ return `
3
+ Expected to find an input element matching the "for" attribute on your label, but we could not locate that input element.
4
+
5
+ for attribute found: "${forAttributeValue}"
6
+
7
+ Make sure you have an input element with an id matching "${forAttributeValue}", otherwise this label
8
+ will not be able to be properly associated with the corresponding input.
9
+ `
10
+ }
@@ -0,0 +1,40 @@
1
+ export default async function captureAttributeFromClosestElementWithType(
2
+ cssSelector: string,
3
+ type: string,
4
+ attribute: string
5
+ ) {
6
+ const el = await page.waitForSelector(cssSelector, { timeout: 3000 })
7
+
8
+ const attributeValue = (await el!.evaluate(
9
+ (element, type, attribute) => {
10
+ // Traverse up the DOM tree to find the closest parent label element
11
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
12
+ let currentElement = element
13
+ let attrVal: string | null = null
14
+ let millisDrift = 0
15
+ const startMillis = Date.now()
16
+
17
+ while (!attrVal && currentElement && millisDrift < 3000) {
18
+ const currMillis = Date.now()
19
+ millisDrift = currMillis - startMillis
20
+
21
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
22
+ if (currentElement.tagName === type.toUpperCase()) {
23
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
24
+ attrVal = Array.from(currentElement.attributes as { name: string; value: string }[]).find(
25
+ attr => attr.name === attribute
26
+ )?.['value'] as string
27
+ } else {
28
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
29
+ currentElement = currentElement.parentElement
30
+ }
31
+ }
32
+
33
+ return attrVal
34
+ },
35
+ type,
36
+ attribute
37
+ )) as string
38
+
39
+ return attributeValue
40
+ }
@@ -1,5 +1,8 @@
1
1
  import { Page, WaitForSelectorOptions } from 'puppeteer'
2
2
  import applyDefaultWaitForOpts from '../helpers/applyDefaultWaitForOpts.js'
3
+ import emptyForAttribute from '../error-messages/emptyForAttribute.js'
4
+ import missingInputToMatchForAttribute from '../error-messages/missingInputToMatchForAttribute.js'
5
+ import captureAttributeFromClosestElementWithType from '../internal/captureAttributeFromClosestElementWithType.js'
3
6
 
4
7
  export default async function toCheck(
5
8
  page: Page,
@@ -11,11 +14,30 @@ export default async function toCheck(
11
14
  message: () => `Expected page to have checkable element with text: "${expectedText}"`,
12
15
  }
13
16
 
17
+ const labelSelector = `label::-p-text("${expectedText}")`
14
18
  try {
15
- await expect(page).toClickSelector(
16
- `label::-p-text("${expectedText}")`,
17
- applyDefaultWaitForOpts(opts)
19
+ const forAttributeValue = await captureAttributeFromClosestElementWithType(
20
+ labelSelector,
21
+ 'label',
22
+ 'for'
18
23
  )
24
+ if (!forAttributeValue) {
25
+ return {
26
+ pass: false,
27
+ message: () => emptyForAttribute(),
28
+ }
29
+ }
30
+
31
+ try {
32
+ await page.waitForSelector(`#${forAttributeValue}`, applyDefaultWaitForOpts(opts))
33
+ } catch {
34
+ return {
35
+ pass: false,
36
+ message: () => missingInputToMatchForAttribute(forAttributeValue),
37
+ }
38
+ }
39
+
40
+ await expect(page).toClickSelector(labelSelector, applyDefaultWaitForOpts(opts))
19
41
  return {
20
42
  pass: true,
21
43
  message: () => {
@@ -23,7 +23,7 @@ export default async function toNotMatchTextContent(
23
23
  successText: () => {
24
24
  throw new Error('Cannot negate toNotMatchTextContent, use toMatchTextContent instead')
25
25
  },
26
- failureText: r => `Expected ${r} to match text ${expected}`,
26
+ failureText: r => `Expected ${r} to not match text ${expected}, but it did`,
27
27
  timeout: opts.timeout,
28
28
  }
29
29
  )
@@ -1,5 +1,8 @@
1
- import { Page, WaitForSelectorOptions } from 'puppeteer'
1
+ import { ElementHandle, Page, WaitForSelectorOptions } from 'puppeteer'
2
2
  import applyDefaultWaitForOpts from '../helpers/applyDefaultWaitForOpts.js'
3
+ import emptyForAttribute from '../error-messages/emptyForAttribute.js'
4
+ import missingInputToMatchForAttribute from '../error-messages/missingInputToMatchForAttribute.js'
5
+ import captureAttributeFromClosestElementWithType from '../internal/captureAttributeFromClosestElementWithType.js'
3
6
 
4
7
  export default async function toUncheck(
5
8
  page: Page,
@@ -8,13 +11,32 @@ export default async function toUncheck(
8
11
  ) {
9
12
  try {
10
13
  const labelSelector = `label::-p-text("${expectedText}")`
11
-
12
- // eslint-disable-next-line
13
- const forAttributeValue = await page.$eval(labelSelector, label => label.getAttribute('for'))
14
- const inputElement = await page.waitForSelector(
15
- `#${forAttributeValue}`,
16
- applyDefaultWaitForOpts(opts)
14
+ const forAttributeValue = await captureAttributeFromClosestElementWithType(
15
+ labelSelector,
16
+ 'label',
17
+ 'for'
17
18
  )
19
+ if (!forAttributeValue) {
20
+ return {
21
+ pass: false,
22
+ message: () => emptyForAttribute(),
23
+ }
24
+ }
25
+
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ let inputElement: ElementHandle<any> | null = null
28
+ try {
29
+ inputElement = await page.waitForSelector(
30
+ `#${forAttributeValue}`,
31
+ applyDefaultWaitForOpts(opts)
32
+ )
33
+ } catch {
34
+ return {
35
+ pass: false,
36
+ message: () => missingInputToMatchForAttribute(forAttributeValue),
37
+ }
38
+ }
39
+
18
40
  // eslint-disable-next-line
19
41
  const isChecked = await page.evaluate(checkbox => checkbox.checked, inputElement)
20
42
  if (!isChecked)