@rvoh/psychic-spec-helpers 0.6.1 → 0.7.1

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 (66) hide show
  1. package/dist/esm/spec/features/check.spec.js +12 -0
  2. package/dist/esm/spec/features/uncheck.spec.js +23 -0
  3. package/dist/esm/spec/unit/OpenapiSpecRequest/delete.spec.js +29 -0
  4. package/dist/esm/spec/unit/OpenapiSpecRequest/get.spec.js +35 -0
  5. package/dist/esm/spec/unit/OpenapiSpecRequest/patch.spec.js +32 -0
  6. package/dist/esm/spec/unit/OpenapiSpecRequest/post.spec.js +30 -0
  7. package/dist/esm/spec/unit/OpenapiSpecRequest/put.spec.js +32 -0
  8. package/dist/esm/spec/unit/OpenapiSpecSession/delete.spec.js +17 -0
  9. package/dist/esm/spec/unit/OpenapiSpecSession/get.spec.js +24 -0
  10. package/dist/esm/spec/unit/OpenapiSpecSession/patch.spec.js +20 -0
  11. package/dist/esm/spec/unit/OpenapiSpecSession/post.spec.js +18 -0
  12. package/dist/esm/spec/unit/OpenapiSpecSession/put.spec.js +20 -0
  13. package/dist/esm/spec/unit/specRequest/helpers/fillOpenapiUrlParams.spec.js +11 -0
  14. package/dist/esm/src/feature/error-messages/emptyForAttribute.js +12 -0
  15. package/dist/esm/src/feature/error-messages/missingInputToMatchForAttribute.js +10 -0
  16. package/dist/esm/src/feature/internal/captureAttributeFromClosestElementWithType.js +26 -0
  17. package/dist/esm/src/feature/matchers/toCheck.js +21 -1
  18. package/dist/esm/src/feature/matchers/toNotMatchTextContent.js +1 -1
  19. package/dist/esm/src/feature/matchers/toUncheck.js +21 -3
  20. package/dist/esm/src/index.js +2 -0
  21. package/dist/esm/src/unit/OpenapiSpecRequest.js +103 -0
  22. package/dist/esm/src/unit/OpenapiSpecSession.js +54 -0
  23. package/dist/esm/src/unit/helpers/fillOpenapiParams.js +8 -0
  24. package/dist/esm/src/unit/helpers/openapiTypeHelpers.js +1 -0
  25. package/dist/esm/src/unit/helpers/typeHelpers.js +1 -0
  26. package/dist/esm/test-app/src/app/controllers/UserController.js +84 -0
  27. package/dist/esm/test-app/src/app/controllers/UsersController.js +107 -0
  28. package/dist/esm/test-app/src/conf/app.js +3 -0
  29. package/dist/esm/test-app/src/conf/routes.js +9 -0
  30. package/dist/types/spec/unit/OpenapiSpecRequest/delete.spec.d.ts +1 -0
  31. package/dist/types/spec/unit/OpenapiSpecRequest/get.spec.d.ts +1 -0
  32. package/dist/types/spec/unit/OpenapiSpecRequest/patch.spec.d.ts +1 -0
  33. package/dist/types/spec/unit/OpenapiSpecRequest/post.spec.d.ts +1 -0
  34. package/dist/types/spec/unit/OpenapiSpecRequest/put.spec.d.ts +1 -0
  35. package/dist/types/spec/unit/OpenapiSpecSession/delete.spec.d.ts +1 -0
  36. package/dist/types/spec/unit/OpenapiSpecSession/get.spec.d.ts +1 -0
  37. package/dist/types/spec/unit/OpenapiSpecSession/patch.spec.d.ts +1 -0
  38. package/dist/types/spec/unit/OpenapiSpecSession/post.spec.d.ts +1 -0
  39. package/dist/types/spec/unit/OpenapiSpecSession/put.spec.d.ts +1 -0
  40. package/dist/types/spec/unit/specRequest/helpers/fillOpenapiUrlParams.spec.d.ts +1 -0
  41. package/dist/types/src/feature/error-messages/emptyForAttribute.d.ts +1 -0
  42. package/dist/types/src/feature/error-messages/missingInputToMatchForAttribute.d.ts +1 -0
  43. package/dist/types/src/feature/internal/captureAttributeFromClosestElementWithType.d.ts +1 -0
  44. package/dist/types/src/index.d.ts +2 -0
  45. package/dist/types/src/unit/OpenapiSpecRequest.d.ts +691 -0
  46. package/dist/types/src/unit/OpenapiSpecSession.d.ts +571 -0
  47. package/dist/types/src/unit/SpecSession.d.ts +2 -18
  48. package/dist/types/src/unit/helpers/fillOpenapiParams.d.ts +1 -0
  49. package/dist/types/src/unit/helpers/openapiTypeHelpers.d.ts +29 -0
  50. package/dist/types/src/unit/helpers/typeHelpers.d.ts +1 -0
  51. package/dist/types/test-app/src/app/controllers/UserController.d.ts +8 -0
  52. package/dist/types/test-app/src/app/controllers/UsersController.d.ts +9 -0
  53. package/package.json +4 -4
  54. package/src/feature/error-messages/emptyForAttribute.ts +12 -0
  55. package/src/feature/error-messages/missingInputToMatchForAttribute.ts +10 -0
  56. package/src/feature/internal/captureAttributeFromClosestElementWithType.ts +40 -0
  57. package/src/feature/matchers/toCheck.ts +25 -3
  58. package/src/feature/matchers/toNotMatchTextContent.ts +1 -1
  59. package/src/feature/matchers/toUncheck.ts +29 -7
  60. package/src/index.ts +2 -0
  61. package/src/unit/OpenapiSpecRequest.ts +1435 -0
  62. package/src/unit/OpenapiSpecSession.ts +1169 -0
  63. package/src/unit/SpecSession.ts +2 -25
  64. package/src/unit/helpers/fillOpenapiParams.ts +8 -0
  65. package/src/unit/helpers/openapiTypeHelpers.ts +79 -0
  66. package/src/unit/helpers/typeHelpers.ts +1 -0
@@ -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,29 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecRequest#delete', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a delete request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ await request.delete('/users/{id}', 204, {
12
+ id: user.id,
13
+ headers: {
14
+ hello: 'world',
15
+ },
16
+ });
17
+ expect(await User.count()).toEqual(0);
18
+ });
19
+ context('without params', () => {
20
+ it('issues a delete request to a controller endpoint, passing when the status matches', async () => {
21
+ await request.delete('/user', 204, {
22
+ headers: {
23
+ hello: 'world',
24
+ },
25
+ });
26
+ await request.delete('/user', 204);
27
+ });
28
+ });
29
+ });
@@ -0,0 +1,35 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecRequest#get', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a get request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ const res = await request.get('/users', 200, { headers: { hello: 'world' } });
12
+ expect(res.body).toEqual([{ id: user.id, email: 'abc@def' }]);
13
+ });
14
+ context('with params', () => {
15
+ it('succeeds', async () => {
16
+ const user = await User.create({ email: 'abc@def' });
17
+ const res = await request.get('/users/{id}', 200, {
18
+ id: user.id,
19
+ headers: { hello: 'world' },
20
+ });
21
+ expect(res.body).toEqual({ id: user.id, email: 'abc@def' });
22
+ });
23
+ });
24
+ context('query params', () => {
25
+ it('issues a get request to a controller endpoint, passing when the status matches', async () => {
26
+ await User.create({ email: 'other@user' });
27
+ const user = await User.create({ email: 'abc@def' });
28
+ const res = await request.get('/users', 200, {
29
+ query: { search: 'abc' },
30
+ headers: { hello: 'world' },
31
+ });
32
+ expect(res.body).toEqual([{ id: user.id, email: 'abc@def' }]);
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,32 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecRequest#patch', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a patch request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ await request.patch('/users/{id}', 204, {
12
+ id: user.id,
13
+ data: {
14
+ email: 'how@yadoin',
15
+ },
16
+ headers: {
17
+ hello: 'world',
18
+ },
19
+ });
20
+ await User.findOrFailBy({ email: 'how@yadoin' });
21
+ });
22
+ context('without params', () => {
23
+ it('issues a patch request to a controller endpoint, passing when the status matches', async () => {
24
+ await request.patch('/user', 204, {
25
+ headers: {
26
+ hello: 'world',
27
+ },
28
+ });
29
+ await request.patch('/user', 204);
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,30 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecRequest#post', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a get request to a controller endpoint, passing when the status matches', async () => {
10
+ await request.post('/users', 201, {
11
+ data: {
12
+ email: 'how@yadoin',
13
+ },
14
+ headers: {
15
+ hello: 'world',
16
+ },
17
+ });
18
+ await User.findOrFailBy({ email: 'how@yadoin' });
19
+ });
20
+ context('without params', () => {
21
+ it('issues a post request to a controller endpoint, passing when the status matches', async () => {
22
+ await request.post('/user', 201, {
23
+ headers: {
24
+ hello: 'world',
25
+ },
26
+ });
27
+ await request.post('/user', 201);
28
+ });
29
+ });
30
+ });
@@ -0,0 +1,32 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecRequest#put', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a patch request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ await request.put('/users/{id}/update-put', 204, {
12
+ id: user.id,
13
+ data: {
14
+ email: 'how@yadoin',
15
+ },
16
+ headers: {
17
+ hello: 'world',
18
+ },
19
+ });
20
+ await User.findOrFailBy({ email: 'how@yadoin' });
21
+ });
22
+ context('without params', () => {
23
+ it('issues a put request to a controller endpoint, passing when the status matches', async () => {
24
+ await request.put('/user/update-put', 204, {
25
+ headers: {
26
+ hello: 'world',
27
+ },
28
+ });
29
+ await request.put('/user/update-put', 204);
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,17 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecSession#delete', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a delete request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ const session = await request.session('get', '/users', 200);
12
+ await session.delete('/users/{id}', 204, {
13
+ id: user.id,
14
+ });
15
+ expect(await User.count()).toEqual(0);
16
+ });
17
+ });
@@ -0,0 +1,24 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecSession#get', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a get request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ const session = await request.session('get', '/users', 200);
12
+ const res = await session.get('/users', 200);
13
+ expect(res.body).toEqual([{ id: user.id, email: 'abc@def' }]);
14
+ });
15
+ context('query params', () => {
16
+ it('issues a get request to a controller endpoint, passing when the status matches', async () => {
17
+ await User.create({ email: 'other@user' });
18
+ const user = await User.create({ email: 'abc@def' });
19
+ const session = await request.session('get', '/users', 200);
20
+ const res = await session.get('/users', 200, { query: { search: 'abc' } });
21
+ expect(res.body).toEqual([{ id: user.id, email: 'abc@def' }]);
22
+ });
23
+ });
24
+ });
@@ -0,0 +1,20 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecSession#put', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a patch request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ const session = await request.session('get', '/users', 200);
12
+ await session.patch('/users/{id}', 204, {
13
+ id: user.id,
14
+ data: {
15
+ email: 'how@yadoin',
16
+ },
17
+ });
18
+ await User.findOrFailBy({ email: 'how@yadoin' });
19
+ });
20
+ });
@@ -0,0 +1,18 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecSession#post', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a get request to a controller endpoint, passing when the status matches', async () => {
10
+ const session = await request.session('get', '/users', 200);
11
+ await session.post('/users', 201, {
12
+ data: {
13
+ email: 'how@yadoin',
14
+ },
15
+ });
16
+ await User.findOrFailBy({ email: 'how@yadoin' });
17
+ });
18
+ });
@@ -0,0 +1,20 @@
1
+ import { PsychicServer } from '@rvoh/psychic';
2
+ import { OpenapiSpecRequest } from '../../../src/unit/OpenapiSpecRequest.js';
3
+ import User from '../../../test-app/src/app/models/User.js';
4
+ const request = new OpenapiSpecRequest();
5
+ describe('OpenapiSpecSession#put', () => {
6
+ beforeEach(async () => {
7
+ await request.init(PsychicServer);
8
+ });
9
+ it('issues a patch request to a controller endpoint, passing when the status matches', async () => {
10
+ const user = await User.create({ email: 'abc@def' });
11
+ const session = await request.session('get', '/users', 200);
12
+ await session.put('/users/{id}/update-put', 204, {
13
+ id: user.id,
14
+ data: {
15
+ email: 'how@yadoin',
16
+ },
17
+ });
18
+ await User.findOrFailBy({ email: 'how@yadoin' });
19
+ });
20
+ });
@@ -0,0 +1,11 @@
1
+ import fillOpenapiParams from '../../../../src/unit/helpers/fillOpenapiParams.js';
2
+ describe('fillOpenapiUrlParams', () => {
3
+ it('pushes params into a route', () => {
4
+ expect(fillOpenapiParams('/users/{id}/pets/{petId}/posts/{postId}', {
5
+ id: 1,
6
+ petId: 2,
7
+ postId: 3,
8
+ otherId: 4,
9
+ })).toEqual('/users/1/pets/2/posts/3');
10
+ });
11
+ });
@@ -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)
@@ -1,6 +1,8 @@
1
1
  // unit spec helpers
2
2
  export { default as specRequest } from './unit/SpecRequest.js';
3
3
  export { default as createPsychicServer } from './unit/createPsychicServer.js';
4
+ export { OpenapiSpecRequest } from './unit/OpenapiSpecRequest.js';
5
+ export { OpenapiSpecSession } from './unit/OpenapiSpecSession.js';
4
6
  export { SpecRequest } from './unit/SpecRequest.js';
5
7
  export { SpecSession } from './unit/SpecSession.js';
6
8
  // feature spec helpers
@@ -0,0 +1,103 @@
1
+ import supertest from 'supertest';
2
+ import { createPsychicServer } from '../index.js';
3
+ import fillOpenapiParams from './helpers/fillOpenapiParams.js';
4
+ import { OpenapiSpecSession } from './OpenapiSpecSession.js';
5
+ import supersession from './supersession.js';
6
+ export class OpenapiSpecRequest {
7
+ // eslint-disable-next-line
8
+ PsychicServer;
9
+ // eslint-disable-next-line
10
+ server;
11
+ // final
12
+ async get(uri, expectedStatus, opts) {
13
+ return (await this.makeRequest('get', fillOpenapiParams(uri, (opts || {})), expectedStatus, opts));
14
+ }
15
+ // final
16
+ async post(uri, expectedStatus, opts = {}) {
17
+ return await this.makeRequest('post', uri, expectedStatus, opts);
18
+ }
19
+ // final
20
+ async put(uri, expectedStatus, opts = {}) {
21
+ return await this.makeRequest('put', fillOpenapiParams(uri, opts || {}), expectedStatus, opts);
22
+ }
23
+ // final
24
+ async patch(uri, expectedStatus, opts = {}) {
25
+ return await this.makeRequest('patch', fillOpenapiParams(uri, opts), expectedStatus, opts);
26
+ }
27
+ // final
28
+ async delete(uri, expectedStatus, opts = {}) {
29
+ return await this.makeRequest('delete', fillOpenapiParams(uri, opts), expectedStatus, opts);
30
+ }
31
+ // eslint-disable-next-line
32
+ async init(PsychicServer) {
33
+ // eslint-disable-next-line
34
+ this.PsychicServer = PsychicServer;
35
+ this.server ||= await createPsychicServer(PsychicServer);
36
+ }
37
+ // final
38
+ async session(httpMethod, uri, expectedStatus, opts = {}) {
39
+ const postOpts = opts;
40
+ const getOpts = opts;
41
+ uri = fillOpenapiParams(uri, (opts || {}));
42
+ return await new Promise((accept, reject) => {
43
+ createPsychicServer(this.PsychicServer)
44
+ .then(server => {
45
+ const session = supersession(server);
46
+ // supersession is borrowed from a non-typescript repo, which
47
+ // does not have strong types around http methods, so we need to any cast
48
+ let req = session[(httpMethod || 'post')](`/${uri.replace(/^\//, '')}`);
49
+ if (postOpts.data) {
50
+ req = req.send(postOpts.data);
51
+ }
52
+ req
53
+ .expect(expectedStatus)
54
+ .query(getOpts.query || {})
55
+ .set(postOpts.headers || {})
56
+ .end((err) => {
57
+ if (err)
58
+ return reject(err);
59
+ return accept(new OpenapiSpecSession(session));
60
+ });
61
+ })
62
+ .catch(err => {
63
+ throw err;
64
+ });
65
+ });
66
+ }
67
+ async makeRequest(method, uri, expectedStatus, opts = {}) {
68
+ // TODO: find out why this is necessary. Currently, without initializing the server
69
+ // at the beginning of the specs, supertest is unable to use our server to handle requests.
70
+ // it gives the appearance of being an issue with a runaway promise (i.e. missing await)
71
+ // but I can't find it anywhere, so I am putting this init method in as a temporary fix.
72
+ if (!this.server)
73
+ throw new Error(`
74
+ ERROR:
75
+ When making use of the send spec helper, you must first call "await specRequest.init(PsychicServer)"
76
+ from a beforEach hook at the root of your specs.
77
+ `);
78
+ if (expectedStatus === 500) {
79
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = '1';
80
+ }
81
+ // eslint-disable-next-line
82
+ const req = supertest.agent(this.server.expressApp);
83
+ let request = req[method](`/${uri.replace(/^\//, '')}`);
84
+ if (opts.headers)
85
+ request = request.set(opts.headers);
86
+ if (opts.query)
87
+ request = request.query(opts.query);
88
+ if (method !== 'get')
89
+ request = request.send(opts.data);
90
+ try {
91
+ const res = await request.expect(expectedStatus);
92
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined;
93
+ return res;
94
+ }
95
+ catch (err) {
96
+ // without manually console logging, you get no stack trace here
97
+ console.error(err);
98
+ console.trace();
99
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined;
100
+ throw err;
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,54 @@
1
+ import fillOpenapiParams from './helpers/fillOpenapiParams.js';
2
+ // like SpecRequest, but meant to be bound to an instance
3
+ // of supersession, enabling chained requests to collect cookies
4
+ export class OpenapiSpecSession {
5
+ _session;
6
+ constructor(_session) {
7
+ this._session = _session;
8
+ }
9
+ // final
10
+ async get(uri, expectedStatus, opts) {
11
+ return (await this.makeRequest('get', fillOpenapiParams(uri, (opts || {})), expectedStatus, opts));
12
+ }
13
+ // final
14
+ async post(uri, expectedStatus, opts = {}) {
15
+ return await this.makeRequest('post', uri, expectedStatus, opts);
16
+ }
17
+ // final
18
+ async put(uri, expectedStatus, opts = {}) {
19
+ return await this.makeRequest('put', fillOpenapiParams(uri, opts || {}), expectedStatus, opts);
20
+ }
21
+ // final
22
+ async patch(uri, expectedStatus, opts = {}) {
23
+ return await this.makeRequest('patch', fillOpenapiParams(uri, opts), expectedStatus, opts);
24
+ }
25
+ // final
26
+ async delete(uri, expectedStatus, opts = {}) {
27
+ return await this.makeRequest('delete', fillOpenapiParams(uri, opts), expectedStatus, opts);
28
+ }
29
+ async makeRequest(method, uri, expectedStatus, opts = {}) {
30
+ if (expectedStatus === 500) {
31
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = '1';
32
+ }
33
+ const req = this._session;
34
+ let request = req[method](`/${uri.replace(/^\//, '')}`);
35
+ if (opts.headers)
36
+ request = request.set(opts.headers);
37
+ if (opts.query)
38
+ request = request.query(opts.query);
39
+ if (method !== 'get')
40
+ request = request.send(opts.data);
41
+ try {
42
+ const res = await request.expect(expectedStatus);
43
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined;
44
+ return res;
45
+ }
46
+ catch (err) {
47
+ // without manually console logging, you get no stack trace here
48
+ console.error(err);
49
+ console.trace();
50
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined;
51
+ throw err;
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,8 @@
1
+ export default function fillOpenapiParams(url, params) {
2
+ const matches = url.matchAll(/\{([^}]*)}/g);
3
+ for (const match of matches) {
4
+ const segment = match[1];
5
+ url = url.replace(`{${segment}}`, params[segment]);
6
+ }
7
+ return url;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};