Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 62 additions & 0 deletions .github/workflows/regression-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Regression Tests

on:
push:
branches:
- '**'

jobs:
regression:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup NVM and Node
run: |
export NVM_DIR="$HOME/.nvm"
mkdir -p "$NVM_DIR"
# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
. "$NVM_DIR/nvm.sh"
. "$NVM_DIR/bash_completion"
# Use Node version from .nvmrc
nvm install
nvm use
node -v

- name: Install Yarn v1
run: |
npm i -g yarn@1
yarn -v

- name: Install dependencies across workspaces
run: |
node shared/install-dependencies.js

- name: Build API
run: |
nvm use
yarn run build:api

- name: Start API (test mode)
run: |
nvm use
yarn run start:api:test &
# Wait for API to be reachable (default port 3001 or similar). If wait-on is present, use it.
# Fallback: sleep to allow server to start.
sleep 10

- name: Start Web client
run: |
nvm use
yarn run dev:web &
# Allow client to boot
sleep 20

- name: Run regression tests
env:
CI: true
run: |
nvm use
yarn run test:regression
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"license": "BSD-3-Clause",
"devDependencies": {
"@babel/preset-flow": "^7.0.0",
"@testing-library/jest-dom": "4.2.4",
"@testing-library/react": "12.1.5",
"babel-cli": "^6.24.1",
"babel-eslint": "^8.2.6",
"babel-jest": "^22.4.4",
Expand All @@ -24,6 +26,7 @@
"circular-dependency-plugin": "4.x",
"cpy-cli": "^2.0.0",
"cross-env": "^5.2.0",
"cross-fetch": "^4.1.0",
"cypress-plugin-retries": "^1.2.0",
"eslint": "^4.1.1",
"eslint-config-react-app": "^2.1.0",
Expand All @@ -38,6 +41,7 @@
"is-html": "^1.1.0",
"lint-staged": "^3.3.0",
"micromatch": "^3.0.4",
"msw": "0.25.0",
"prettier": "^1.14.3",
"raw-loader": "^0.5.1",
"react-app-rewire-hot-loader": "^1.0.3",
Expand Down Expand Up @@ -249,7 +253,8 @@
"lint:staged": "lint-staged",
"webpack-defaults": "webpack-defaults",
"deploy": "node scripts/deploy",
"heroku": "node scripts/heroku-deploy"
"heroku": "node scripts/heroku-deploy",
"test:regression": "jest --config regression-tests/jest.config.js"
},
"lint-staged": {
"*.js": [
Expand Down
8 changes: 8 additions & 0 deletions regression-tests/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { rest } = require('msw');

// Define request handlers here as needed for tests
const handlers = [
// Example: rest.get('/api/ping', (req, res, ctx) => res(ctx.json({ ok: true })))
];

module.exports = { handlers, rest };
149 changes: 149 additions & 0 deletions regression-tests/index.render.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Regression test for src/index.js render
const React = require('react');

// Mock modules that index.js relies on but are not needed for render assertion
jest.mock('offline-plugin/runtime', () => ({
install: jest.fn(() => {}),
applyUpdate: jest.fn(() => {}),
}));

// Mock apollo client export used by src/index.js
jest.mock('../shared/graphql', () => {
return {
client: {},
wsLink: {
subscriptionClient: {
on: jest.fn(),
},
},
};
});

// Mock history to have predictable location/search
jest.mock('../src/helpers/history', () => {
const location = { pathname: '/', search: '' };
return {
history: {
location,
replace: jest.fn(path => {
// update location for any redirects
const [pathname, search] = path.split('?');
location.pathname = pathname;
location.search = search ? `?${search}` : '';
}),
listen: jest.fn(),
push: jest.fn(),
},
};
});

// Mock web push manager setter to avoid accessing service worker
jest.mock('../src/helpers/web-push-manager', () => ({ set: jest.fn() }));

// Mock RedirectHandler's child routes to a simple component for easier assertion
jest.mock('../src/hot-routes', () => () =>
React.createElement('div', { 'data-testid': 'routes' }, 'Routes')
);

describe('src/index.js render', () => {
let rootEl;
let originalSW;
let originalPushManager;

beforeEach(() => {
// Ensure a root element exists for ReactDOM to mount into
rootEl = document.createElement('div');
rootEl.setAttribute('id', 'root');
document.body.appendChild(rootEl);

// Ensure service worker and PushManager presence branches are predictable
originalSW = navigator.serviceWorker;
originalPushManager = window.PushManager;
// Minimal stub to satisfy feature detection without triggering ready callbacks
Object.defineProperty(navigator, 'serviceWorker', {
value: { ready: Promise.resolve({ pushManager: {} }) },
configurable: true,
});
Object.defineProperty(window, 'PushManager', {
value: function() {},
configurable: true,
});

// Clear server state by default to use ReactDOM.render path
delete window.__SERVER_STATE__;
});

afterEach(() => {
// Cleanup DOM
if (rootEl && rootEl.parentNode) rootEl.parentNode.removeChild(rootEl);
// Restore globals
Object.defineProperty(navigator, 'serviceWorker', {
value: originalSW,
configurable: true,
});
Object.defineProperty(window, 'PushManager', {
value: originalPushManager,
configurable: true,
});
jest.resetModules();
jest.clearAllMocks();
});

test('renders application root and attaches event listeners', async () => {
// Spy on ReactDOM.render and hydrate to observe usage
const renderSpy = jest
.spyOn(require('react-dom'), 'render')
.mockImplementation(() => {});
const hydrateSpy = jest
.spyOn(require('react-dom'), 'hydrate')
.mockImplementation(() => {});

// Import after mocks so index executes with mocks in place
await import('../src/index.js');

// Either render or hydrate should have been called once with the App and #root
expect(renderSpy.mock.calls.length + hydrateSpy.mock.calls.length).toBe(1);
const call = renderSpy.mock.calls[0] || hydrateSpy.mock.calls[0];
expect(call[1]).toBe(rootEl);

// Assert that our simple routes placeholder is present in the tree
// Since we stubbed ReactDOM to no-op, we can at least ensure the element exists pre-render
// Trigger a real render now to verify DOM output without side effects
renderSpy.mockRestore();
const { render } = require('@testing-library/react');
const App = call[0];
const { getByTestId } = render(App);
expect(getByTestId('routes')).toBeInTheDocument();

// Verify websocket event listeners got registered
const { wsLink } = require('../shared/graphql');
expect(wsLink.subscriptionClient.on).toHaveBeenCalledTimes(3);
expect(wsLink.subscriptionClient.on).toHaveBeenCalledWith(
'disconnected',
expect.any(Function)
);
expect(wsLink.subscriptionClient.on).toHaveBeenCalledWith(
'connected',
expect.any(Function)
);
expect(wsLink.subscriptionClient.on).toHaveBeenCalledWith(
'reconnected',
expect.any(Function)
);
});

test('hydrates when __SERVER_STATE__ is present', async () => {
window.__SERVER_STATE__ = {};
const renderSpy = jest
.spyOn(require('react-dom'), 'render')
.mockImplementation(() => {});
const hydrateSpy = jest
.spyOn(require('react-dom'), 'hydrate')
.mockImplementation(() => {});

await import('../src/index.js');

expect(hydrateSpy).toHaveBeenCalledTimes(1);
expect(renderSpy).not.toHaveBeenCalled();
});
});
7 changes: 7 additions & 0 deletions regression-tests/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
testEnvironment: 'jsdom',
setupTestFrameworkScriptFile: '<rootDir>/setupTests.js',
testMatch: ['**/*.test.js'],
rootDir: '.',
testURL: 'http://localhost/',
};
Loading
Loading