Skip to content

Commit b555580

Browse files
author
Ole Martin Handeland
committed
Merge branch 'main' into bug/feedback-step
# Conflicts: # src/features/datamodel/DataModelsProvider.tsx
2 parents 8b9a3c8 + ff46fdf commit b555580

File tree

17 files changed

+655
-557
lines changed

17 files changed

+655
-557
lines changed

.yarn/releases/yarn-4.9.2.cjs renamed to .yarn/releases/yarn-4.9.3.cjs

Lines changed: 357 additions & 357 deletions
Large diffs are not rendered by default.

.yarnrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ enableTelemetry: false
44

55
nodeLinker: node-modules
66

7-
yarnPath: .yarn/releases/yarn-4.9.2.cjs
7+
yarnPath: .yarn/releases/yarn-4.9.3.cjs

cypress.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const CYPRESS_WINDOW_HEIGHT = env.parsed?.CYPRESS_WINDOW_HEIGHT || 1080;
1111
module.exports = defineConfig({
1212
e2e: {
1313
setupNodeEvents(on, config) {
14+
require('cypress-terminal-report/src/installLogsPrinter')(on, { printLogsToConsole: 'always' });
1415
on('before:browser:launch', (browser, launchOptions) => {
1516
if (browser.name === 'electron') {
1617
launchOptions.preferences.width = CYPRESS_WINDOW_WIDTH;

package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@
3939
"@babel/runtime": "7.28.3",
4040
"@babel/runtime-corejs3": "7.28.3",
4141
"@eslint/compat": "1.3.2",
42-
"@faker-js/faker": "9.9.0",
42+
"@faker-js/faker": "10.0.0",
4343
"@percy/cli": "1.31.1",
4444
"@percy/cypress": "3.1.6",
4545
"@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
4646
"@tanstack/react-query-devtools": "5.85.5",
4747
"@testing-library/cypress": "10.0.3",
4848
"@testing-library/dom": "10.4.1",
49-
"@testing-library/jest-dom": "6.7.0",
49+
"@testing-library/jest-dom": "6.8.0",
5050
"@testing-library/react": "16.3.0",
5151
"@testing-library/user-event": "14.6.1",
5252
"@types/dot-object": "2.1.6",
@@ -57,7 +57,7 @@
5757
"@types/marked": "6.0.0",
5858
"@types/mime": "4.0.0",
5959
"@types/node": "22.17.2",
60-
"@types/react": "19.1.10",
60+
"@types/react": "19.1.11",
6161
"@types/react-dom": "19.1.7",
6262
"@types/react-router-dom": "5.3.3",
6363
"@types/uuid": "10.0.0",
@@ -66,8 +66,8 @@
6666
"axe-core": "4.10.3",
6767
"babel-jest": "30.0.5",
6868
"babel-loader": "10.0.0",
69-
"caniuse-lite": "1.0.30001735",
70-
"core-js": "3.45.0",
69+
"caniuse-lite": "1.0.30001737",
70+
"core-js": "3.45.1",
7171
"cross-env": "10.0.0",
7272
"css-loader": "7.1.2",
7373
"cypress": "14.5.4",
@@ -77,10 +77,11 @@
7777
"cypress-multi-reporters": "2.0.5",
7878
"cypress-network-idle": "1.15.0",
7979
"cypress-plugin-tab": "1.0.5",
80+
"cypress-terminal-report": "^7.2.1",
8081
"cypress-wait-until": "3.0.2",
8182
"dotenv": "17.2.1",
8283
"esbuild-loader": "4.3.0",
83-
"eslint": "9.33.0",
84+
"eslint": "9.34.0",
8485
"eslint-config-prettier": "10.1.8",
8586
"eslint-plugin-cypress": "5.1.1",
8687
"eslint-plugin-import": "2.32.0",
@@ -119,10 +120,10 @@
119120
"terser-webpack-plugin": "5.3.14",
120121
"tinybench": "4.1.0",
121122
"ts-jest": "29.4.1",
122-
"ts-loader": "9.5.2",
123+
"ts-loader": "9.5.4",
123124
"ts-node": "10.9.2",
124125
"tsconfig-paths": "4.2.0",
125-
"tsx": "4.20.4",
126+
"tsx": "4.20.5",
126127
"typescript-plugin-css-modules": "5.2.0",
127128
"use-immer": "0.11.0",
128129
"utility-types": "3.11.0",
@@ -176,10 +177,10 @@
176177
"typescript": "5.9.2",
177178
"typescript-eslint": "8.40.0",
178179
"uuid": "11.1.0",
179-
"zod": "4.0.17",
180-
"zustand": "5.0.7"
180+
"zod": "4.1.1",
181+
"zustand": "5.0.8"
181182
},
182-
"packageManager": "[email protected].2",
183+
"packageManager": "[email protected].3",
183184
"lint-staged": {
184185
"*.{js,jsx,ts,tsx}": [
185186
".husky/pre-commit-check-for-skipped-tests",

scripts/debug-flaky-cypress.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/bin/bash
2+
3+
# Script to run specified Cypress test a certain number of times (defaults to 50) or until failure.
4+
# Writes logs to the $logs_folder
5+
6+
spec_arg=${1:-""} # Optional spec argument- defaults to running all tests if not specified
7+
max_runs=${2:-50} # Default to 50 if no argument provided
8+
counter=1
9+
logs_folder=scripts/debug-flaky-cypress-logs
10+
11+
# Handle Ctrl+C gracefully
12+
cleanup() {
13+
echo ""
14+
echo "Interrupted by user after $((counter-1)) runs"
15+
# Kill any running cypress processes
16+
pkill -f "cypress"
17+
exit 0
18+
}
19+
20+
trap cleanup INT TERM
21+
22+
if [ -n "$spec_arg" ]; then
23+
echo "Running test up to $max_runs times with spec: $spec_arg"
24+
else
25+
echo "Running test up to $max_runs times..."
26+
fi
27+
28+
# If logs_folder doesn't exist, create it
29+
mkdir -p $logs_folder;
30+
31+
while [ $counter -le $max_runs ]; do
32+
echo "=== Run #$counter/$max_runs ==="
33+
34+
# Run the command and capture output
35+
if [ -n "$spec_arg" ]; then
36+
yarn cy:run --spec "$spec_arg" 2>&1 | tee "$logs_folder/run-$counter.log"
37+
else
38+
yarn cy:run 2>&1 | tee "$logs_folder/run-$counter.log"
39+
fi
40+
exit_code=$?
41+
echo "Exit code for run #$counter: $exit_code"
42+
43+
# Check for test failures in the output
44+
if grep -q "Failing:.*[1-9]" "$logs_folder/run-$counter.log" || \
45+
grep -q "✖.*failed" "$logs_folder/run-$counter.log" || \
46+
grep -q "failed (100%)" "$logs_folder/run-$counter.log"; then
47+
echo ""
48+
echo "==================================="
49+
echo "FAILURE DETECTED ON RUN #$counter"
50+
echo "==================================="
51+
echo "Test failed after $counter attempts"
52+
echo "Check $logs_folder/run-$counter.log for details"
53+
echo ""
54+
exit 0
55+
fi
56+
57+
# Also check exit code as backup
58+
if [ $exit_code -ne 0 ]; then
59+
echo ""
60+
echo "==================================="
61+
echo "FAILURE DETECTED ON RUN #$counter"
62+
echo "==================================="
63+
echo "Test failed with exit code $exit_code on the $counter attempt"
64+
echo "Check $logs_folder/run-$counter.log for details"
65+
echo ""
66+
exit 0 # Exit the function, not the script
67+
fi
68+
69+
echo "Run #$counter passed"
70+
counter=$((counter + 1))
71+
done
72+
73+
echo "All $max_runs runs passed!"
74+
return 0

src/core/contexts/queryContext.tsx

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { UseQueryResult } from '@tanstack/react-query';
55
import type { AxiosError } from 'axios';
66

77
import { createContext } from 'src/core/contexts/context';
8-
import { DisplayError as DefaultDisplayError } from 'src/core/errorHandling/DisplayError';
9-
import { Loader as DefaultLoader } from 'src/core/loading/Loader';
8+
import { DisplayError } from 'src/core/errorHandling/DisplayError';
9+
import { Loader } from 'src/core/loading/Loader';
1010
import type { LaxContextProps, StrictContextProps } from 'src/core/contexts/context';
1111

1212
type Err = Error | AxiosError;
@@ -18,17 +18,9 @@ type Query<Req extends boolean, QueryData> = () => Req extends true
1818

1919
type ContextProps<Ctx, Req extends boolean> = Req extends true ? StrictContextProps : LaxContextProps<Ctx>;
2020

21-
export type QueryContextProps<QueryData, Req extends boolean, ContextData = QueryData> = ContextProps<
22-
ContextData,
23-
Req
24-
> & {
21+
export type QueryContextProps<QueryData, Req extends boolean> = ContextProps<QueryData, Req> & {
2522
query: Query<Req, QueryData>;
26-
27-
process?: (data: QueryData) => ContextData;
2823
shouldDisplayError?: (error: Err) => boolean;
29-
30-
DisplayError?: React.ComponentType<{ error: Err }>;
31-
Loader?: React.ComponentType<{ reason: string }>;
3224
};
3325

3426
/**
@@ -38,26 +30,16 @@ export type QueryContextProps<QueryData, Req extends boolean, ContextData = Quer
3830
* Remember to call this through a delayedContext() call to prevent problems with cyclic imports.
3931
* @see delayedContext
4032
*/
41-
export function createQueryContext<QD, Req extends boolean, CD = QD>(props: QueryContextProps<QD, Req, CD>) {
42-
const {
43-
name,
44-
required,
45-
query,
46-
process = (i: QD) => i as unknown as CD,
47-
shouldDisplayError = () => true,
48-
DisplayError = DefaultDisplayError,
49-
Loader = DefaultLoader,
50-
...rest
51-
} = props;
33+
export function createQueryContext<QD, Req extends boolean>(props: QueryContextProps<QD, Req>) {
34+
const { name, required, query: useQuery, shouldDisplayError = () => true, ...rest } = props;
5235
// eslint-disable-next-line @typescript-eslint/no-explicit-any
53-
const { Provider, useCtx, useLaxCtx, useHasProvider } = createContext<CD>({ name, required, ...(rest as any) });
54-
const defaultValue = ('default' in rest ? rest.default : undefined) as CD;
36+
const { Provider, useCtx, useLaxCtx, useHasProvider } = createContext<QD>({ name, required, ...(rest as any) });
37+
const defaultValue = ('default' in rest ? rest.default : undefined) as QD;
5538

56-
const WrappingProvider = ({ children }: PropsWithChildren) => {
57-
const { data, isPending, error, ...rest } = query();
39+
function WrappingProvider({ children }: PropsWithChildren) {
40+
const { data, isPending, error, ...rest } = useQuery();
5841
const enabled = 'enabled' in rest && !required ? rest.enabled : true;
59-
60-
const value = useMemo(() => (typeof data !== 'undefined' ? process(data) : undefined), [data]);
42+
const value = useMemo(() => data, [data]);
6143

6244
if (enabled && isPending) {
6345
return <Loader reason={`query-${name}`} />;
@@ -68,7 +50,7 @@ export function createQueryContext<QD, Req extends boolean, CD = QD>(props: Quer
6850
}
6951

7052
return <Provider value={enabled ? (value ?? defaultValue) : defaultValue}>{children}</Provider>;
71-
};
53+
}
7254

7355
return {
7456
Provider: WrappingProvider,

src/features/datamodel/DataModelsProvider.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { useCallback, useEffect, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
22
import type { PropsWithChildren } from 'react';
33

4+
import { useMutationState } from '@tanstack/react-query';
45
import deepEqual from 'fast-deep-equal';
56
import { createStore } from 'zustand';
67
import type { JSONSchema7 } from 'json-schema';
@@ -241,6 +242,18 @@ function BlockUntilLoaded({ children }: PropsWithChildren) {
241242
const actualCurrentTask = useCurrentLayoutSetId();
242243
const isPDF = useIsPdf();
243244

245+
const currentMutations = useMutationState({ filters: { status: 'pending', mutationKey: ['saveFormData'] } });
246+
const hasPassedMutationCheck = useRef(false);
247+
if (currentMutations.length > 0 && !hasPassedMutationCheck.current) {
248+
// FormDataWrite automatically saves unsaved changes on unmount. If something happens above us in the render tree
249+
// that causes FormDataWrite to be unmounted (forcing it to save) and re-mounts everything (including us), we
250+
// should wait for that previously started save to complete. Otherwise, we'd end up saving outdated initial data
251+
// and cause a 409 when patching later.
252+
return <Loader reason='save-form-data' />;
253+
}
254+
255+
hasPassedMutationCheck.current = true;
256+
244257
if (error) {
245258
// Error trying to fetch data, if missing rights we display relevant page
246259
if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Forbidden) {

src/features/form/layout/LayoutsContext.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useMemo } from 'react';
22

33
import { skipToken, useQuery } from '@tanstack/react-query';
44

@@ -57,15 +57,18 @@ function useLayoutQuery() {
5757
utils.error && window.logError('Fetching form layout failed:\n', utils.error);
5858
}, [utils.error]);
5959

60-
return utils.data
61-
? {
62-
...utils,
63-
data: {
64-
...utils.data,
65-
lookups: makeLayoutLookups(utils.data.layouts),
66-
},
67-
}
68-
: utils;
60+
const data = useMemo(() => {
61+
if (utils.data) {
62+
return {
63+
...utils.data,
64+
lookups: makeLayoutLookups(utils.data.layouts),
65+
};
66+
}
67+
68+
return utils.data;
69+
}, [utils.data]);
70+
71+
return { ...utils, data };
6972
}
7073
const { Provider, useCtx, useLaxCtx } = delayedContext(() =>
7174
createQueryContext({

src/features/form/layoutSets/LayoutSetsProvider.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@ export function useLayoutSetsQueryDef() {
1414
const { fetchLayoutSets } = useAppQueries();
1515
return {
1616
queryKey: ['fetchLayoutSets'],
17-
queryFn: fetchLayoutSets,
17+
queryFn: async () => {
18+
const layoutSets = await fetchLayoutSets();
19+
if (layoutSets?.uiSettings?.taskNavigation) {
20+
return {
21+
...layoutSets,
22+
uiSettings: {
23+
...layoutSets.uiSettings,
24+
taskNavigation: layoutSets.uiSettings.taskNavigation.map((g) => ({ ...g, id: uuidv4() })),
25+
},
26+
};
27+
}
28+
return layoutSets;
29+
},
1830
};
1931
}
2032

@@ -33,18 +45,6 @@ const { Provider, useCtx, useLaxCtx } = delayedContext(() =>
3345
name: 'LayoutSets',
3446
required: true,
3547
query: useLayoutSetsQuery,
36-
process: (layoutSets) => {
37-
if (layoutSets?.uiSettings?.taskNavigation) {
38-
return {
39-
...layoutSets,
40-
uiSettings: {
41-
...layoutSets.uiSettings,
42-
taskNavigation: layoutSets.uiSettings.taskNavigation.map((g) => ({ ...g, id: uuidv4() })),
43-
},
44-
};
45-
}
46-
return layoutSets;
47-
},
4848
}),
4949
);
5050

0 commit comments

Comments
 (0)