Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(add integration tests against CMS) #1446

Merged
merged 30 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
517955f
add verify cms content test
Spencer6497 Nov 15, 2024
cf90e36
Merge branch 'main' into verify-cms
Spencer6497 Nov 15, 2024
dfd44b9
enable testing of strapi for xstate entries
Spencer6497 Nov 15, 2024
5c2780f
Merge branch 'main' into verify-cms
Spencer6497 Nov 15, 2024
4099758
add custom jest matcher, fix recursive logic, structure tests more cl…
Spencer6497 Nov 15, 2024
782d962
Merge branch 'main' into verify-cms
Spencer6497 Nov 15, 2024
2501fce
remove jest extender, add test for field names in xstate, restructure…
Spencer6497 Nov 15, 2024
782defc
re-add custom jest matcher
Spencer6497 Nov 18, 2024
08e577a
Merge branch 'main' into verify-cms
Spencer6497 Nov 18, 2024
eef367a
remove unused xstate pages from fluggastrechte and geldeinklagen flow…
Spencer6497 Nov 18, 2024
7b2abed
add recursive zod key function, test strapi form fields against it
Spencer6497 Nov 18, 2024
7d7d2fb
move integration test to appropriate folder and add to vitest setup
Spencer6497 Nov 19, 2024
aa88d52
Merge branch 'main' into verify-cms
Spencer6497 Nov 19, 2024
d2d5bd6
add test file
Spencer6497 Nov 19, 2024
7cffc65
fix all failing unit tests
Spencer6497 Nov 19, 2024
623c4c7
Merge branch 'main' into verify-cms
Spencer6497 Nov 19, 2024
bbc8118
only run verifyCmsContent on CI
Spencer6497 Nov 19, 2024
ad85854
fix type declaration file
Spencer6497 Nov 19, 2024
195a083
add run-integration-tests step to CI pipeline, add vitest workspace s…
Spencer6497 Nov 19, 2024
4134950
re-add vitest run to npm scripts instead of test
Spencer6497 Nov 19, 2024
6e3d550
Merge branch 'main' into verify-cms
Spencer6497 Nov 19, 2024
8b903b0
add checkout step to job
Spencer6497 Nov 19, 2024
8a36b91
add download-artifact step to access content.json
Spencer6497 Nov 19, 2024
dad1189
remove comments, pull out utils to discrete file
Spencer6497 Nov 19, 2024
fa6d014
clean up util file, add unit tests for utilities
Spencer6497 Nov 19, 2024
08761ad
address PR comments
Spencer6497 Nov 20, 2024
18a5136
Merge branch 'main' into verify-cms
Spencer6497 Nov 20, 2024
0d1075c
Merge branch 'main' into verify-cms
Spencer6497 Nov 20, 2024
9b87f3b
Merge branch 'main' into verify-cms
Spencer6497 Nov 20, 2024
801da0c
Merge branch 'main' into verify-cms
Spencer6497 Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ jobs:
outputs:
content_checksum: ${{ steps.checksum.outputs.content_checksum }}

verify-local-e2e:
run-integration-tests:
needs: [get-content-file]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cached-checkout-install
- uses: actions/download-artifact@v4
with:
name: content-file
- run: npm run test:integration
Spencer6497 marked this conversation as resolved.
Show resolved Hide resolved

verify-local-e2e:
needs: [run-integration-tests]
uses: ./.github/workflows/e2e-test.yml
with:
require-published-app: false
Expand Down
10 changes: 10 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
/// <reference types="vite/client" />
/// <reference types="@remix-run/node" />
import type { Assertion } from "vitest";

interface CustomMatchers<R = unknown> {
toContainStrapiPage: (strapiPage: string | null) => R;
}

declare module "vitest" {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@
"size": "remix vite:build --logLevel error && size-limit",
"start:storybook": "storybook dev -p 6006",
"start": "NODE_ENV=production node ./server.js",
"test:coverage": "vitest run --coverage",
"test:coverage": "vitest run --project unit --coverage",
"test:e2e:ui": "npx playwright test --ui",
"test:e2e": "npx playwright test",
"test": "vitest run",
"test:watch": "vitest watch",
"test": "vitest run --project unit",
"test:watch": "vitest run --project unit watch",
"test:integration": "vitest run --project integration",
"typecheck": "tsc",
"typecheck:watch": "tsc -w",
"update:courtData": "tsx ./app/services/gerichtsfinder/encryptedStorage.ts updateZip",
Expand Down
181 changes: 181 additions & 0 deletions tests/integration/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { z } from "zod";
import {
compileAllStrapiPages,
getAllPossibleStates,
ignoreList,
invertStrapiFormFields,
zodKeys,
} from "tests/integration/util";

describe("integration testing helper functions", () => {
describe("compileAllStrapiPages", () => {
it("should return a flat array of all strapi page names", () => {
const result = compileAllStrapiPages("/beratungshilfe/antrag", {
"/beratungshilfe/antrag": {
"vorab-check-pages": [
{
// @ts-expect-error missing attributes
attributes: {
stepId: "step-1",
locale: "de",
},
},
],
"form-flow-pages": [
{
// @ts-expect-error missing attributes
attributes: {
stepId: "step-2",
locale: "de",
},
},
],
"result-pages": [
{
// @ts-expect-error missing attributes
attributes: {
stepId: "step-3",
locale: "de",
},
},
],
},
});

expect(result.length).toBe(3);
expect(result).toEqual(
expect.arrayContaining(["step-1", "step-2", "step-3"]),
);
});

it("should filter out pages with locale other than the default locale", () => {
const result = compileAllStrapiPages("/beratungshilfe/antrag", {
"/beratungshilfe/antrag": {
"vorab-check-pages": [
{
// @ts-expect-error missing attributes
attributes: {
stepId: "step-1",
locale: "en",
},
},
{
// @ts-expect-error missing attributes
attributes: {
stepId: "step-1",
locale: "en",
},
},
],
"form-flow-pages": [],
"result-pages": [],
},
});
expect(result.length).toBe(0);
});
});

describe("zodKeys", () => {
it("should recursively return all keys of a nested zod schema", () => {
const schemaKeys = zodKeys(
z.object({
a: z.string(),
b: z.number(),
c: z.object({ d: z.string() }),
e: z.array(z.object({ f: z.string() })),
}),
);

expect(schemaKeys).toEqual(["a", "b", "c.d", "e#f"]);
});

it("should return an empty array when schema is null or undefined", () => {
// @ts-expect-error
expect(zodKeys(null)).toEqual([]);
// @ts-expect-error
expect(zodKeys(undefined)).toEqual([]);
});

it("should return the keys of a nested object inside of an array", () => {
const schemaKeys = zodKeys(
z.object({
a: z.array(
z.object({ b: z.string(), c: z.object({ d: z.string() }) }),
),
}),
);
expect(schemaKeys).toEqual(["a#b", "a#c.d"]);
});
});

describe("allPossibleStates", () => {
it("should return an array of all possible states", () => {
const allPossibleStates = getAllPossibleStates({
config: {
states: {
state1: {},
state2: {
states: {
state3: {},
},
},
},
},
flowType: "formFlow",
guards: {},
});
expect(allPossibleStates).toEqual([
"/state1",
"/state2",
"/state2/state3",
]);
});

it.each(ignoreList)("should skip state %s", (ignoredPath: string) => {
const allPossibleStates = getAllPossibleStates({
config: {
states: {
[ignoredPath]: {},
},
},
flowType: "formFlow",
guards: {},
});
expect(allPossibleStates).toEqual([]);
});

it("should skip redundant states with an initial state", () => {
const allPossibleStates = getAllPossibleStates({
config: {
states: {
state1: {},
state2: {
initial: "state3",
states: {
state3: {},
},
},
},
},
flowType: "formFlow",
guards: {},
});
expect(allPossibleStates).toEqual(["/state1", "/state2/state3"]);
});
});

describe("invertStrapiFormFields", () => {
it("should reverse a FormFieldsMap to list the field names first, then the paths", () => {
const result = invertStrapiFormFields({
field: ["path1", "path2"],
field2: ["path3", "path4"],
});
expect(result).toEqual([
["path1", "field"],
["path2", "field"],
["path3", "field2"],
["path4", "field2"],
]);
});
});
});
106 changes: 106 additions & 0 deletions tests/integration/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { z } from "zod";
import type { FlowId } from "~/domains/flowIds";
import type { Flow } from "~/domains/flows.server";
import type { FormFieldsMap } from "~/services/cms/fetchAllFormFields";
import { defaultLocale } from "~/services/cms/models/StrapiLocale";
import { flowPageSchemas } from "~/services/cms/schemas";

const _flowPageSchemas = z.object(flowPageSchemas);
type FlowPageSchemas = z.infer<typeof _flowPageSchemas>;
type StrapiPages = FlowPageSchemas & { formFields: FormFieldsMap };
export type AllStrapiData = Record<FlowId, StrapiPages>;

/**
* These pages do not exist in strapi and instead exist in our own codebase
*/
export const ignoreList = [
"redirect-vorabcheck-ergebnis",
"partner-start",
"abschluss",
"unterhalt", // antragstellende person unterhalt
];

export function compileAllStrapiPages(
flowId: FlowId,
allStrapiData: AllStrapiData,
) {
const {
"vorab-check-pages": vorabCheckPages,
"form-flow-pages": formFlowPages,
"result-pages": resultPages,
} = allStrapiData[flowId];
return [...formFlowPages, ...resultPages, ...vorabCheckPages]
.filter((page) => page.attributes.locale === defaultLocale)
.map((page) => page.attributes.stepId);
}

/**
* Helper Function to recursively retrieve all keys from a zod schema
* Massive if branching is needed, as Zod has different ways of encoding
* a schema's keys for each data type
*/
export function zodKeys<T extends z.ZodTypeAny>(schema: T): string[] {
// make sure schema is not null or undefined
Spencer6497 marked this conversation as resolved.
Show resolved Hide resolved
if (schema === null || schema === undefined) return [];
// check if schema is nullable or optional
if (schema instanceof z.ZodNullable || schema instanceof z.ZodOptional)
return zodKeys(schema.unwrap());
// check if schema is an array
if (schema instanceof z.ZodArray) {
return zodKeys(schema.element);
}
// check if schema is a ZodEffect
if (schema instanceof z.ZodEffects) {
return zodKeys(schema._def?.schema ?? schema);
}
// check if schema is an object
if (schema instanceof z.ZodObject) {
// get key/value pairs from schema
const entries = Object.entries(schema.shape);
// loop through key/value pairs
return entries.flatMap(([key, value]) => {
// get nested keys
const nested =
value instanceof z.ZodType
? zodKeys(value).map(
(subKey) =>
`${key}${value instanceof z.ZodArray ? "#" : "."}${subKey}`,
)
: [];
// return nested keys
return nested.length ? nested : key;
});
}
// return empty array
return [];
}

export function getAllPossibleStates(flow: Flow) {
const allPossibleStates: string[] = [];

function _getAllPossibleStates(
states: Flow["config"]["states"],
path: string,
) {
for (const state in states) {
if (ignoreList.includes(state)) continue;
const stateString = `${path}/${state.replace(/^ergebnis\//, "")}`;
if (!states[state].initial) {
allPossibleStates.push(stateString);
}
if (states[state].states) {
_getAllPossibleStates(states[state].states, stateString);
}
}
}

_getAllPossibleStates(flow.config.states, "");

return allPossibleStates;
}

export function invertStrapiFormFields(formFields: FormFieldsMap) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is technically a general function, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. was torn about where to locate it, but decided on tests/integration specifically as we're only using it currently in integration tests. would be happy to relocate it to somewhere like fetchAllFormFields.ts

return Object.entries(formFields)
.map(([key, values]) => values.map((value) => [value, key]))
.flat();
}
Loading
Loading