@ministryofjustice/hmpps-probation-integration-e2e-tests 1.149.0 → 1.150.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ministryofjustice/hmpps-probation-integration-e2e-tests",
3
3
  "description": "Playwright end to end tests for hmpps-probation-integration-services.",
4
- "version": "1.149.0",
4
+ "version": "1.150.0",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ministryofjustice/hmpps-probation-integration-e2e-tests#readme",
7
7
  "bugs": {
@@ -41,6 +41,7 @@
41
41
  "./steps/api/cvl/cvl-api": "./steps/api/cvl/cvl-api.mjs",
42
42
  "./steps/api/dps/prison-api": "./steps/api/dps/prison-api.mjs",
43
43
  "./steps/api/epf/epf-api": "./steps/api/epf/epf-api.mjs",
44
+ "./steps/api/esupervision/esupervision": "./steps/api/esupervision/esupervision.mjs",
44
45
  "./steps/api/external-api/external-api": "./steps/api/external-api/external-api.mjs",
45
46
  "./steps/api/prison-identifier-and-delius/prison-identifier-and-delius": "./steps/api/prison-identifier-and-delius/prison-identifier-and-delius.mjs",
46
47
  "./steps/api/resettlement-passport/resettlement-passport-api": "./steps/api/resettlement-passport/resettlement-passport-api.mjs",
@@ -144,13 +145,14 @@
144
145
  "./steps/delius/utils/refresh": "./steps/delius/utils/refresh.mjs",
145
146
  "./steps/dps/caseload": "./steps/dps/caseload.mjs",
146
147
  "./steps/dps/login": "./steps/dps/login.mjs",
147
- "./steps/esupervision/check-in": "./steps/esupervision/check-in.mjs",
148
148
  "./steps/hdc-licences/login": "./steps/hdc-licences/login.mjs",
149
149
  "./steps/hmpps-auth/login": "./steps/hmpps-auth/login.mjs",
150
150
  "./steps/k8s/k8s-utils": "./steps/k8s/k8s-utils.mjs",
151
151
  "./steps/make-recall-decisions/flags": "./steps/make-recall-decisions/flags.mjs",
152
152
  "./steps/make-recall-decisions/login": "./steps/make-recall-decisions/login.mjs",
153
153
  "./steps/make-recall-decisions/start-recommendation": "./steps/make-recall-decisions/start-recommendation.mjs",
154
+ "./steps/manage-a-supervision/application": "./steps/manage-a-supervision/application.mjs",
155
+ "./steps/manage-a-supervision/check-in": "./steps/manage-a-supervision/check-in.mjs",
154
156
  "./steps/manage-a-supervision/login": "./steps/manage-a-supervision/login.mjs",
155
157
  "./steps/manage-pom-cases/caseload": "./steps/manage-pom-cases/caseload.mjs",
156
158
  "./steps/manage-pom-cases/login": "./steps/manage-pom-cases/login.mjs",
@@ -0,0 +1,4 @@
1
+ //#region steps/api/esupervision/esupervision.d.ts
2
+ declare function createOffenderCheckin(crn: string, dueDate: string): Promise<string>;
3
+ //#endregion
4
+ export { createOffenderCheckin };
@@ -0,0 +1,28 @@
1
+ import { getToken } from "../auth/get-token.mjs";
2
+ import { expect, request } from "@playwright/test";
3
+
4
+ //#region steps/api/esupervision/esupervision.ts
5
+ async function getEsupervisionContext() {
6
+ const token = await getToken();
7
+ return request.newContext({
8
+ baseURL: process.env.ESUPERVISION_API,
9
+ extraHTTPHeaders: {
10
+ Accept: "application/json",
11
+ "Content-Type": "application/json",
12
+ Authorization: `Bearer ${token}`
13
+ }
14
+ });
15
+ }
16
+ async function createOffenderCheckin(crn, dueDate) {
17
+ const response = await (await getEsupervisionContext()).post("/v2/offender_checkins/crn", { data: {
18
+ practitioner: "AutomatedTestUser",
19
+ offender: crn,
20
+ dueDate
21
+ } });
22
+ expect(response.ok(), await response.text()).toBeTruthy();
23
+ const { uuid } = await response.json();
24
+ return uuid;
25
+ }
26
+
27
+ //#endregion
28
+ export { createOffenderCheckin };
@@ -3,5 +3,6 @@ import { DateTime } from "luxon";
3
3
  //#region steps/common/common.d.ts
4
4
  declare const splitDate: (date: DateTime) => string[];
5
5
  declare const slow: (minutes?: number) => void;
6
+ declare const qa: (id: string) => string;
6
7
  //#endregion
7
- export { slow, splitDate };
8
+ export { qa, slow, splitDate };
@@ -14,6 +14,7 @@ const slow = (minutes = 7) => {
14
14
  const duration = Duration.fromObject({ minutes });
15
15
  test.setTimeout(duration.as("milliseconds"));
16
16
  };
17
+ const qa = (id) => `[data-qa="${id}"]`;
17
18
 
18
19
  //#endregion
19
- export { slow, splitDate };
20
+ export { qa, slow, splitDate };
@@ -7,6 +7,12 @@ declare const DeliusDateFormatter: (date: Date) => string;
7
7
  declare const OasysDateFormatter: (date: Date) => string;
8
8
  declare const DeliusTimeFormatter: (time: Date) => string;
9
9
  declare const get12Hour: (date: Date) => number;
10
+ /**
11
+ Returns a working-day date (Mon–Fri only) in "d/M/yyyy" format.
12
+ @param offsetWorkingDays Number of working days to move (+1, -1, +10, etc.)
13
+ @param baseDate Optional base date (defaults to today)
14
+ */
15
+ declare function getWorkingDayForEsupervision(offsetWorkingDays: number, baseDate?: DateTime): string;
10
16
  declare const getTimeOfDay: (date: Date) => "am" | "pm";
11
17
  declare const Tomorrow: DateTime<true>;
12
18
  declare const NextWeek: DateTime<true>;
@@ -24,5 +30,6 @@ declare const formatDate: (date: Date | DateTime, formatString: string) => strin
24
30
  declare const minutesToMilliseconds: (minutes: number) => number;
25
31
  declare const secondsToMilliseconds: (seconds: number) => number;
26
32
  declare function getCurrentDay(): string;
33
+ declare function uiDateToIso(uiDate: string): string;
27
34
  //#endregion
28
- export { DeliusDateFormatter, DeliusTimeFormatter, HmppsDateFormatter, LastMonth, NextMonth, NextWeek, OasysDateFormatter, Tomorrow, Yesterday, addDays, addMonths, formatDate, get12Hour, getCurrentDay, getDate, getMonth, getTimeOfDay, getYear, minutesToMilliseconds, options, secondsToMilliseconds, subDays, subMonths };
35
+ export { DeliusDateFormatter, DeliusTimeFormatter, HmppsDateFormatter, LastMonth, NextMonth, NextWeek, OasysDateFormatter, Tomorrow, Yesterday, addDays, addMonths, formatDate, get12Hour, getCurrentDay, getDate, getMonth, getTimeOfDay, getWorkingDayForEsupervision, getYear, minutesToMilliseconds, options, secondsToMilliseconds, subDays, subMonths, uiDateToIso };
@@ -17,6 +17,34 @@ const get12Hour = (date) => {
17
17
  const dt = DateTime.fromJSDate(date);
18
18
  return dt.hour > 12 ? dt.hour - 12 : dt.hour;
19
19
  };
20
+ /**
21
+ Returns a working-day date (Mon–Fri only) in "d/M/yyyy" format.
22
+ @param offsetWorkingDays Number of working days to move (+1, -1, +10, etc.)
23
+ @param baseDate Optional base date (defaults to today)
24
+ */
25
+ function getWorkingDayForEsupervision(offsetWorkingDays, baseDate = DateTime.now().setZone("Europe/London")) {
26
+ let current = baseDate.startOf("day");
27
+ if (offsetWorkingDays === 0) {
28
+ current = moveToWorkingDay(current, 1);
29
+ return current.toFormat("d/M/yyyy");
30
+ }
31
+ const direction = offsetWorkingDays > 0 ? 1 : -1;
32
+ const steps = Math.abs(offsetWorkingDays);
33
+ let moved = 0;
34
+ while (moved < steps) {
35
+ current = current.plus({ days: direction });
36
+ if (isWeekday(current)) moved++;
37
+ }
38
+ return current.toFormat("d/M/yyyy");
39
+ }
40
+ function isWeekday(date) {
41
+ return date.weekday >= 1 && date.weekday <= 5;
42
+ }
43
+ function moveToWorkingDay(date, direction) {
44
+ let d = date;
45
+ while (!isWeekday(d)) d = d.plus({ days: direction });
46
+ return d;
47
+ }
20
48
  const getTimeOfDay = (date) => DateTime.fromJSDate(date).hour < 12 ? "am" : "pm";
21
49
  const Tomorrow = DateTime.now().plus({ days: 1 });
22
50
  const NextWeek = DateTime.now().plus({ weeks: 1 });
@@ -60,6 +88,11 @@ const secondsToMilliseconds = (seconds) => {
60
88
  function getCurrentDay() {
61
89
  return DateTime.now().weekdayLong;
62
90
  }
91
+ function uiDateToIso(uiDate) {
92
+ const dt = DateTime.fromFormat(uiDate.trim(), "d/M/yyyy", { zone: "Europe/London" });
93
+ if (!dt.isValid) throw new Error(`Invalid UI date "${uiDate}". Luxon error: ${dt.invalidExplanation ?? dt.invalidReason}`);
94
+ return dt.toFormat("yyyy-MM-dd");
95
+ }
63
96
 
64
97
  //#endregion
65
- export { DeliusDateFormatter, DeliusTimeFormatter, HmppsDateFormatter, LastMonth, NextMonth, NextWeek, OasysDateFormatter, Tomorrow, Yesterday, addDays, addMonths, formatDate, get12Hour, getCurrentDay, getDate, getMonth, getTimeOfDay, getYear, minutesToMilliseconds, options, secondsToMilliseconds, subDays, subMonths };
98
+ export { DeliusDateFormatter, DeliusTimeFormatter, HmppsDateFormatter, LastMonth, NextMonth, NextWeek, OasysDateFormatter, Tomorrow, Yesterday, addDays, addMonths, formatDate, get12Hour, getCurrentDay, getDate, getMonth, getTimeOfDay, getWorkingDayForEsupervision, getYear, minutesToMilliseconds, options, secondsToMilliseconds, subDays, subMonths, uiDateToIso };
@@ -0,0 +1,6 @@
1
+ import { Page } from "@playwright/test";
2
+
3
+ //#region steps/manage-a-supervision/application.d.ts
4
+ declare function searchPersonInMPoP(page: Page, crn: string, heading?: ReturnType<Page['locator']>): Promise<void>;
5
+ //#endregion
6
+ export { searchPersonInMPoP };
@@ -0,0 +1,14 @@
1
+ import { doUntil } from "../delius/utils/refresh.mjs";
2
+ import { expect } from "@playwright/test";
3
+
4
+ //#region steps/manage-a-supervision/application.ts
5
+ async function searchPersonInMPoP(page, crn, heading) {
6
+ await page.getByRole("link", { name: "Search" }).click();
7
+ await page.getByLabel("Find a person on probation").fill(crn);
8
+ await doUntil(() => page.getByRole("button", { name: "Search" }).click(), () => expect(page.locator("#search-results-container")).toContainText(crn));
9
+ await page.locator(`[href$="${crn}"]`).first().click();
10
+ if (heading) await expect(heading).toContainText(/Overview/i);
11
+ }
12
+
13
+ //#endregion
14
+ export { searchPersonInMPoP };
@@ -0,0 +1,9 @@
1
+ import { Person } from "../delius/utils/person.mjs";
2
+ import { Page } from "@playwright/test";
3
+
4
+ //#region steps/manage-a-supervision/check-in.d.ts
5
+ declare function registerCaseInMPoP(page: Page, person: Person, crn: string): Promise<string>;
6
+ declare function createCheckin(page: Page, uuid: string, person: Person): Promise<void>;
7
+ declare function reviewCheckinInMPoP(page: Page, crn: string): Promise<void>;
8
+ //#endregion
9
+ export { createCheckin, registerCaseInMPoP, reviewCheckinInMPoP };
@@ -0,0 +1,108 @@
1
+ import { getWorkingDayForEsupervision, options, uiDateToIso } from "../delius/utils/date-time.mjs";
2
+ import { qa } from "../common/common.mjs";
3
+ import { refreshUntil } from "../delius/utils/refresh.mjs";
4
+ import { searchPersonInMPoP } from "./application.mjs";
5
+ import { login } from "./login.mjs";
6
+ import { createOffenderCheckin } from "../api/esupervision/esupervision.mjs";
7
+ import { expect } from "@playwright/test";
8
+ import { DateTime } from "luxon";
9
+
10
+ //#region steps/manage-a-supervision/check-in.ts
11
+ async function registerCaseInMPoP(page, person, crn) {
12
+ const uiDueDate = getWorkingDayForEsupervision(1);
13
+ const apiDueDate = uiDateToIso(uiDueDate);
14
+ const heading = page.locator(qa("pageHeading"));
15
+ await login(page);
16
+ await searchPersonInMPoP(page, crn, heading);
17
+ await expect(page.locator(qa("crn"))).toContainText(crn);
18
+ await expect(page.locator(qa("name"))).toContainText(`${person.firstName} ${person.lastName}`);
19
+ await page.getByRole("link", {
20
+ name: "Appointments",
21
+ exact: true
22
+ }).click();
23
+ await page.getByRole("link", { name: "Set up online check ins" }).click();
24
+ await expect(heading).toContainText("How you can use online check ins");
25
+ await page.getByRole("button", { name: "Continue" }).click();
26
+ await expect(heading).toContainText(/Set up\s+online check ins/i);
27
+ await page.locator(".moj-js-datepicker-input").fill(uiDueDate);
28
+ await page.getByRole("radio", { name: "Every 2 weeks" }).check();
29
+ await page.getByRole("button", { name: "Continue" }).click();
30
+ await expect(heading).toContainText(/Contact preferences/i);
31
+ await page.getByRole("button", { name: /change\s+email\s+address/i }).click();
32
+ await expect(heading).toContainText(new RegExp(`Edit contact details for ${person.firstName}`, "i"));
33
+ const email = `${person.lastName}@service.gov.uk`;
34
+ await page.getByRole("textbox", { name: /Email address/i }).fill(email);
35
+ await page.getByRole("button", { name: /Save changes/i }).click();
36
+ await expect(page.locator(".moj-banner__message")).toContainText("Contact details saved");
37
+ await page.getByRole("radio", { name: "Email" }).check();
38
+ await page.getByRole("button", { name: "Continue" }).click();
39
+ await expect(heading).toContainText(new RegExp(`Take a photo of ${person.firstName}`, "i"));
40
+ await page.getByRole("radio", { name: "Upload a photo" }).check();
41
+ await page.getByRole("button", { name: "Continue" }).click();
42
+ await expect(heading).toContainText(new RegExp(`Upload a photo of ${person.firstName}`, "i"));
43
+ await page.locator("input[type=\"file\"]").setInputFiles("files/mock-person-photo.png");
44
+ await page.getByRole("button", { name: "Continue" }).click();
45
+ await expect(heading).toContainText("Does this photo meet the rules?");
46
+ await page.getByRole("link", { name: /Yes,\s*continue/i }).click();
47
+ await expect(heading).toContainText(/Check your answers before adding/i);
48
+ await page.getByRole("button", { name: "Confirm" }).click();
49
+ await expect(page.locator(".govuk-panel--confirmation")).toContainText("Online check ins added");
50
+ return await createOffenderCheckin(crn, apiDueDate);
51
+ }
52
+ async function createCheckin(page, uuid, person) {
53
+ await page.goto(`${process.env.PROBATION_CHECK_IN_URL}/${uuid}`);
54
+ await page.getByRole("button", { name: "Start now" }).click();
55
+ await enterPersonalDetails(page, person);
56
+ await page.getByRole("button", { name: "Continue" }).click();
57
+ await page.getByRole("radio", { name: "Very well" }).check();
58
+ await page.getByRole("button", { name: "Continue" }).click();
59
+ await page.getByRole("checkbox", { name: "No, I do not need help" }).check();
60
+ await page.getByRole("button", { name: "Continue" }).click();
61
+ await page.getByRole("radio", { name: "No" }).check();
62
+ await page.getByRole("button", { name: "Continue" }).click();
63
+ await page.getByRole("button", { name: "Continue" }).click();
64
+ await page.getByRole("button", { name: "Start recording" }).click();
65
+ await page.getByRole("button", { name: /Submit video anyway/ }).click();
66
+ await page.getByRole("checkbox", { name: /I confirm/ }).check();
67
+ await page.getByRole("button", { name: "Complete check in" }).click();
68
+ await expect(page.locator("h1")).toContainText("Check in completed");
69
+ }
70
+ async function enterPersonalDetails(page, person) {
71
+ await page.getByRole("textbox", { name: "First name" }).fill(person.firstName);
72
+ await page.getByRole("textbox", { name: "Last name" }).fill(person.lastName);
73
+ const dob = DateTime.fromJSDate(person.dob);
74
+ await page.getByRole("textbox", { name: "Day" }).fill(dob.day.toString());
75
+ await page.getByRole("textbox", { name: "Month" }).fill(dob.month.toString());
76
+ await page.getByRole("textbox", { name: "Year" }).fill(dob.year.toString());
77
+ }
78
+ async function reviewCheckinInMPoP(page, crn) {
79
+ const heading = page.locator(qa("pageHeading"));
80
+ await page.goto(process.env.MANAGE_PEOPLE_ON_PROBATION_URL);
81
+ await searchPersonInMPoP(page, crn, heading);
82
+ await page.getByRole("link", {
83
+ name: "Contacts",
84
+ exact: true
85
+ }).click();
86
+ await expect(heading).toContainText("Contacts");
87
+ await refreshUntil(page, async () => {
88
+ await expect(page.getByRole("link", { name: "Online probation check in" })).toBeVisible();
89
+ }, options);
90
+ await page.getByRole("link", {
91
+ name: "Online probation check in",
92
+ exact: true
93
+ }).click();
94
+ await expect(heading).toContainText("Online check in submitted");
95
+ await page.getByRole("radio", { name: "Yes" }).check();
96
+ await page.getByRole("button", { name: "Confirm and review responses" }).click();
97
+ await expect(heading).toContainText("Online check in submitted");
98
+ await page.getByRole("button", { name: "Confirm review" }).click();
99
+ await expect(page.locator(qa("descriptionValue"))).toContainText("Online check in completed");
100
+ const checkinCard = page.locator("section.app-summary-card", { has: page.getByRole("link", { name: /online probation check in/i }) });
101
+ await refreshUntil(page, async () => {
102
+ await expect(checkinCard).toBeVisible();
103
+ await expect(checkinCard).toContainText(/Checkin status:\s*Reviewed/i);
104
+ }, options);
105
+ }
106
+
107
+ //#endregion
108
+ export { createCheckin, registerCaseInMPoP, reviewCheckinInMPoP };
@@ -1,9 +0,0 @@
1
- import { Person } from "../delius/utils/person.mjs";
2
- import { Page } from "@playwright/test";
3
-
4
- //#region steps/esupervision/check-in.d.ts
5
- declare function registerCase(page: Page, person: Person, crn: string): Promise<string>;
6
- declare function createCheckin(page: Page, uuid: string, person: Person): Promise<void>;
7
- declare function reviewCheckin(page: Page, person: Person): Promise<void>;
8
- //#endregion
9
- export { createCheckin, registerCase, reviewCheckin };
@@ -1,70 +0,0 @@
1
- import { DeliusDateFormatter } from "../delius/utils/date-time.mjs";
2
- import { login } from "../hmpps-auth/login.mjs";
3
- import { expect } from "@playwright/test";
4
- import { DateTime } from "luxon";
5
-
6
- //#region steps/esupervision/check-in.ts
7
- async function registerCase(page, person, crn) {
8
- await login(page);
9
- await page.goto(process.env.ESUPERVISION_URL);
10
- await page.getByRole("button", { name: "Add person" }).click();
11
- await enterPersonalDetails(page, person);
12
- await page.getByRole("textbox", { name: /CRN/ }).fill(crn);
13
- await page.getByRole("button", { name: "Continue" }).click();
14
- await page.getByRole("link", { name: "Upload photo instead" }).click();
15
- await page.locator("#photoUpload-input").setInputFiles("files/mock-person-photo.png");
16
- await page.getByRole("button", { name: "Upload photo" }).click();
17
- await page.getByRole("link", { name: "Yes, continue" }).click();
18
- await page.getByRole("radio", { name: "Email" }).check();
19
- await page.getByRole("button", { name: "Continue" }).click();
20
- await page.locator("#email").fill(`simulate-delivered+${person.firstName}${person.lastName}@notifications.service.gov.uk`);
21
- await page.getByRole("button", { name: "Continue" }).click();
22
- await page.locator("#startDate").fill(DeliusDateFormatter(/* @__PURE__ */ new Date()));
23
- await page.getByRole("radio", { name: "Every 8 weeks" }).check();
24
- await page.getByRole("button", { name: "Continue" }).click();
25
- await page.getByRole("button", { name: "Confirm and add person" }).click();
26
- await page.getByRole("link", { name: "Awaiting check in" }).click();
27
- const cell = page.getByRole("cell", {
28
- name: person.firstName + " " + person.lastName,
29
- exact: true
30
- });
31
- return await page.getByRole("row").filter({ has: cell }).getAttribute("data-checkin-uuid");
32
- }
33
- async function createCheckin(page, uuid, person) {
34
- await page.goto(`${process.env.ESUPERVISION_URL}/submission/${uuid}`);
35
- await page.getByRole("button", { name: "Start now" }).click();
36
- await enterPersonalDetails(page, person);
37
- await page.getByRole("button", { name: "Continue" }).click();
38
- await page.getByRole("radio", { name: "Very well" }).check();
39
- await page.getByRole("button", { name: "Continue" }).click();
40
- await page.getByRole("checkbox", { name: "No, I do not need help" }).check();
41
- await page.getByRole("button", { name: "Continue" }).click();
42
- await page.getByRole("radio", { name: "No" }).check();
43
- await page.getByRole("button", { name: "Continue" }).click();
44
- await page.getByRole("button", { name: "Continue" }).click();
45
- await page.getByRole("button", { name: "Start recording" }).click();
46
- await page.getByRole("button", { name: /Submit video anyway/ }).click();
47
- await page.getByRole("checkbox", { name: /I confirm/ }).check();
48
- await page.getByRole("button", { name: "Complete check in" }).click();
49
- await expect(page.locator("h1")).toContainText("Check in completed");
50
- }
51
- async function enterPersonalDetails(page, person) {
52
- await page.getByRole("textbox", { name: "First name" }).fill(person.firstName);
53
- await page.getByRole("textbox", { name: "Last name" }).fill(person.lastName);
54
- const dob = DateTime.fromJSDate(person.dob);
55
- await page.getByRole("textbox", { name: "Day" }).fill(dob.day.toString());
56
- await page.getByRole("textbox", { name: "Month" }).fill(dob.month.toString());
57
- await page.getByRole("textbox", { name: "Year" }).fill(dob.year.toString());
58
- }
59
- async function reviewCheckin(page, person) {
60
- await page.goto(process.env.ESUPERVISION_URL);
61
- await page.getByRole("row").filter({ has: page.getByRole("cell", {
62
- name: person.firstName + " " + person.lastName,
63
- exact: true
64
- }) }).getByRole("link", { name: /Review/ }).click();
65
- await page.getByRole("radio", { name: "Yes" }).check();
66
- await page.getByRole("button", { name: "Mark as reviewed" }).click();
67
- }
68
-
69
- //#endregion
70
- export { createCheckin, registerCase, reviewCheckin };