Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c5a2325
wip: Initial commit
Nov 5, 2025
3cce921
chore: prepare upgrade to React 17.0.2 (WIP)
Nov 5, 2025
683afcf
chore(regression-tests): add jest.config.js for regression tests
Nov 5, 2025
8aa5821
feat(regression-tests): add setupTests for RTL and MSW server
Nov 5, 2025
1bb0fd7
feat(tests): add MSW server setup for regression tests
Nov 5, 2025
f8be9c8
test(regression-tests): add MSW health handler
Nov 5, 2025
28fd03a
test: add regression tests for Button component
Nov 5, 2025
1f639b6
chore: add test:regression script to package.json
Nov 5, 2025
ee03ff1
chore: add testing-library deps and msw; update yarn.lock
Nov 5, 2025
03c7025
ci: add regression tests workflow
Nov 5, 2025
9f289fd
chore(regression-tests): update jest.config.js to use setupTestFramew…
Nov 5, 2025
334a5ec
chore(jest): update setupTests path in jest.config.js
Nov 5, 2025
75fd693
chore(tests): switch to @testing-library/jest-dom
Nov 5, 2025
196f8ba
fix(regression-tests): use setupTests.js for setupTestFrameworkScript…
Nov 5, 2025
da92364
refactor(jest): use setupFiles instead of setupTestFrameworkScriptFile
Nov 5, 2025
0558b1c
refactor(tests): switch setupTests to CommonJS require
Nov 5, 2025
d8b124b
refactor(testServer): switch to CommonJS and export server
Nov 5, 2025
f30cfaa
refactor: convert testHandlers to CommonJS and adjust exports
Nov 5, 2025
fec8871
chore(jest): switch to setupTestFrameworkScriptFile for Jest 22
Nov 5, 2025
773dea6
chore(regression-tests): simplify jest setup path to setupTests.js
Nov 5, 2025
e090d4a
chore(jest): resolve setupTests.js path in config
Nov 5, 2025
1206d5a
test(regression): update jest.config.js with rootDir and testMatch
Nov 5, 2025
58d9040
chore: add moduleNameMapper aliases for shared and api
Nov 5, 2025
b208ce8
chore(test): set up regression test suite to support React 17 upgrade…
Nov 5, 2025
1e5b593
feat(scripts): add find_react_components.py to detect React components
Nov 5, 2025
58c72f8
test(regression): add logout component regression test
Nov 5, 2025
0eb1652
test(thread-container): add regression tests for ThreadContainer
Nov 5, 2025
6db4a6a
test: add ChannelSettings regression tests; adjust userSettings test …
Nov 5, 2025
2c61bbd
test: replace redux-mock-store with inline mock store
Nov 5, 2025
e96cdcb
test: wrap MessagesSubscriber tests with MemoryRouter
Nov 5, 2025
1edc06d
test: unwrap ChannelSettings for regression tests; improve router moc…
Nov 5, 2025
4aed23d
refactor(channelSettings): export ChannelSettings component
Nov 5, 2025
b96cf88
test: wrap MessagesSubscriber regression tests with ApolloProvider
Nov 5, 2025
a7b0103
test(thread.container): update regression test to assert error view r…
Nov 5, 2025
b3e1ed0
test: add minimal Apollo client and wrap CommunityMembersSettings tes…
Nov 5, 2025
dab463e
test(regression): adjust ErrorView test to rely on text instead of da…
Nov 5, 2025
de7b854
test(regression): wrap CommunityMembersSettings tests with MemoryRouter
Nov 5, 2025
ba94fb3
test(userSettings): include username in currentUser for regression test
Nov 5, 2025
99437ca
test(user-settings): update currentUser shape to include username
Nov 5, 2025
d2e8cd4
test(regression-tests): remove container assertion in userSettings test
Nov 5, 2025
864e5a3
test(regression-tests): update mocks and expectations for UI labels a…
Nov 5, 2025
1c619e5
test: add regression tests for CommunityMembers component
Nov 5, 2025
da1c75a
test: add regression tests for MutationWrapper component
Nov 5, 2025
d7badd2
refactor(tests): remove per-test userEvent setup and use userEvent di…
Nov 5, 2025
9aec13a
test(regression): use userEvent.click directly and remove setup in Mu…
Nov 5, 2025
3fd651f
test: enable redux-thunk in mutationWrapper regression tests
Nov 5, 2025
220f53f
test: update MutationWrapper regression to expect render called when …
Nov 5, 2025
3e92def
test(regression): wrap communityMembers component with ThemeProvider
Nov 5, 2025
4b2edce
test: use userEvent directly in CommunityMembers regression test
Nov 5, 2025
223c7ea
test: adjust CommunityMembers tests to locate filters as li elements
Nov 5, 2025
5b01e05
test(regression): replace active attribute checks with style checks f…
Nov 5, 2025
5a1810e
test(homeViewRedirect): add regression tests for loading and redirects
Nov 5, 2025
7bcf936
refactor(userSettings): export UserSettings class
Nov 5, 2025
d16a706
test(user-view): add regression tests for UserView
Nov 5, 2025
ceb039d
test(regression-tests): switch to named export for UserSettings import
Nov 5, 2025
a3f1ddc
test: add networkStatus to MessagesSubscriber test data
Nov 5, 2025
8cea086
test: include networkStatus in MessagesSubscriber regression data
Nov 5, 2025
26f7f9b
test: mock viewNetworkHandler to pass through isLoading/isFetchingMor…
Nov 5, 2025
b06e187
test: relax check for 'Return to profile' in userSettings regression
Nov 5, 2025
3978155
test(userSettings): verify 'Return to profile' via getAllByText
Nov 5, 2025
21b9098
test: wrap CommunityList tests with MemoryRouter
Nov 5, 2025
21b34c5
test: include communityPermissions in thread fixture
Nov 5, 2025
50f04e4
refactor: wrap tests with ThemeProvider; export CommunityList; extend…
Nov 5, 2025
7dfca67
test: wrap UserView regression tests with ThemeProvider and theme
Nov 5, 2025
3625994
test: wrap CommunityList tests in MemoryRouter
Nov 5, 2025
42751e8
test: wrap CommunityList regression tests with ThemeProvider
Nov 5, 2025
9e1c28e
test(authViewHandler): add regression tests
Nov 5, 2025
65cd614
refactor(tests): adjust theme import path
Nov 5, 2025
3e7088e
test: add regression tests for CommunityView rendering logic
Nov 5, 2025
f5c58d4
test(community-feeds): add regression tests for CommunityFeeds
Nov 5, 2025
fe40b08
test: render unwrapped AuthViewHandler in regression tests
Nov 5, 2025
89315b7
test(regression-tests): mock recompose/compose to apply HOCs left-to-…
Nov 5, 2025
da501a7
test: add sessionStorage mock for jsdom in regression tests
Nov 5, 2025
e0aa72b
test(regression-tests): add sessionStorage mock for jsdom
Nov 5, 2025
440d624
chore(deps): update react to 17.0.2 and react-dom to 17.0.2
Nov 5, 2025
8f8de7c
chore: update yarn.lock
Nov 5, 2025
085f4cb
refactor(hot-routes): remove react-hot-loader; export plain Routes
Nov 5, 2025
23d4fc9
chore(config): remove react-hot-loader usage
Nov 5, 2025
ab6253f
chore(deps): remove react-hot-loader and react-app-rewire-hot-loader
Nov 5, 2025
83aa8c4
chore: update yarn.lock
Nov 5, 2025
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
23 changes: 23 additions & 0 deletions .github/workflows/regression-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Regression Tests

on:
push:
branches: [ alpha ]
pull_request:
branches: [ alpha ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci || npm install
- name: Run regression tests
run: npm run test:regression
2 changes: 0 additions & 2 deletions config-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const debug = require('debug')('build:config-overrides');
const webpack = require('webpack');
const { injectBabelPlugin } = require('react-app-rewired');
const rewireStyledComponents = require('react-app-rewire-styled-components');
const rewireReactHotLoader = require('react-app-rewire-hot-loader');
const swPrecachePlugin = require('sw-precache-webpack-plugin');
const fs = require('fs');
const path = require('path');
Expand Down Expand Up @@ -79,7 +78,6 @@ module.exports = function override(config, env) {
}
if (process.env.NODE_ENV === 'development') {
config.output.path = path.join(__dirname, './build');
config = rewireReactHotLoader(config, env);
config.plugins.push(
WriteFilePlugin({
log: true,
Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"license": "BSD-3-Clause",
"devDependencies": {
"@babel/preset-flow": "^7.0.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"babel-cli": "^6.24.1",
"babel-eslint": "^8.2.6",
"babel-jest": "^22.4.4",
Expand Down Expand Up @@ -38,10 +41,10 @@
"is-html": "^1.1.0",
"lint-staged": "^3.3.0",
"micromatch": "^3.0.4",
"msw": "^0.35.0",
"prettier": "^1.14.3",
"raw-loader": "^0.5.1",
"react-app-rewire-hot-loader": "^1.0.3",
"react-hot-loader": "^4.12.13",

"react-scripts": "^1.1.5",
"rimraf": "^2.6.1",
"sw-precache-webpack-plugin": "^0.11.4",
Expand Down Expand Up @@ -152,13 +155,13 @@
"query-string": "5.1.1",
"raf": "^3.4.0",
"raven": "^2.6.4",
"react": "^16.8.4",
"react": "^17.0.2",
"react-apollo": "^2.5.1",
"react-app-rewire-styled-components": "^3.0.0",
"react-app-rewired": "1.6.2",
"react-async-hook": "^1.0.0",
"react-clipboard.js": "^2.0.1",
"react-dom": "npm:@hot-loader/react-dom",
"react-dom": "^17.0.2",
"react-dropzone": "^8.0.3",
"react-flip-move": "^3.0.2",
"react-helmet-async": "^0.2.0",
Expand Down Expand Up @@ -249,7 +252,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 -c ./regression-tests/jest.config.js --runInBand"
},
"lint-staged": {
"*.js": [
Expand Down
124 changes: 124 additions & 0 deletions regression-tests/authViewHandler.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
// Import the raw component by reaching into the file and using named export pattern.
// The module exports default wrapped component; to test without Apollo, require the file and access the class.
const AuthModule = require('../src/views/authViewHandler/index.js');
const AuthViewHandler =
AuthModule.__esModule && AuthModule.default
? AuthModule.default.WrappedComponent ||
AuthModule.AuthViewHandler ||
AuthModule.default
: AuthModule.AuthViewHandler || AuthModule;
// Fallback: if wrapped default exposes `WrappedComponent`, prefer it, else assume exported class name
const Unwrapped =
AuthModule.default && AuthModule.default.WrappedComponent
? AuthModule.default.WrappedComponent
: AuthViewHandler;

// Helper: render with mocked HOCs props injection
const renderWithProps = ({
user = null,
loading = false,
pathname = '/',
editUser = jest.fn(),
children = authed => <div>authed:{String(authed)}</div>,
} = {}) => {
const history = { replace: jest.fn() };
const location = { pathname };

const ui = (
<MemoryRouter initialEntries={[pathname]}>
<Unwrapped
history={history}
location={location}
editUser={editUser}
data={{ user, loading }}
>
{children}
</Unwrapped>
</MemoryRouter>
);

const utils = render(ui);
return { ...utils, history, editUser };
};

describe('AuthViewHandler regression', () => {
it('renders children(false) when no user and not loading', () => {
renderWithProps({ user: null, loading: false });
expect(screen.getByText('authed:false')).toBeInTheDocument();
});

it('returns null while loading when no user', () => {
const { container } = renderWithProps({ user: null, loading: true });
expect(container.firstChild).toBeNull();
});

it('renders NewUserOnboarding when user exists without username', () => {
// Mock NewUserOnboarding to avoid HOCs (Apollo/Redux) requirements
jest.doMock('../src/views/newUserOnboarding', () => () => (
<div data-testid="new-user-onboarding">onboarding</div>
));
const Module = require('../src/views/authViewHandler/index.js');
const Raw =
Module.default && Module.default.WrappedComponent
? Module.default.WrappedComponent
: Module.AuthViewHandler || Module.default;

const history = { replace: jest.fn() };
const location = { pathname: '/' };
render(
<MemoryRouter>
<Raw
history={history}
location={location}
editUser={jest.fn()}
data={{ user: { id: 'u1', username: null }, loading: false }}
>
{authed => <div>authed:{String(authed)}</div>}
</Raw>
</MemoryRouter>
);
expect(screen.getByTestId('new-user-onboarding')).toBeInTheDocument();
});

it('calls children(true) when user has id and username', () => {
renderWithProps({ user: { id: 'u1', username: 'alice' } });
expect(screen.getByText('authed:true')).toBeInTheDocument();
});

it('on first user load: sets timezone if missing and redirects from /home', () => {
// initial render with no user
const { rerender, editUser, history } = renderWithProps({
user: null,
loading: false,
pathname: '/home',
});

// simulate user arriving without timezone
const userNoTz = { id: 'u1', username: 'alice', timezone: null };
const location = { pathname: '/home' };
const next = (
<MemoryRouter initialEntries={['/home']}>
<Unwrapped
history={history}
location={location}
editUser={editUser}
data={{ user: userNoTz, loading: false }}
>
{authed => <div>authed:{String(authed)}</div>}
</Unwrapped>
</MemoryRouter>
);
rerender(next);

// editUser should be called with a timezone value
expect(editUser).toHaveBeenCalled();
const arg = editUser.mock.calls[0][0];
expect(typeof arg.timezone).toBe('number');

// history.replace should be called to redirect away from /home
expect(history.replace).toHaveBeenCalledWith('/');
});
});
27 changes: 27 additions & 0 deletions regression-tests/button.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../src/components/button';

describe('Button component regression', () => {
it('renders children and disables when isLoading', async () => {
render(<Button isLoading>Click me</Button>);
const btn = screen.getByText('Click me');
expect(btn).toBeInTheDocument();
// When isLoading, underlying StyledButton receives disabled
expect(btn).toHaveAttribute('disabled');
});

it('wraps with anchor when href is provided and sets rel/target', async () => {
render(
<Button href="https://example.com" target="_blank">
External
</Button>
);
const link = screen.getByRole('link', { name: /external/i });
expect(link).toHaveAttribute('href', 'https://example.com');
expect(link).toHaveAttribute('target', '_blank');
// rel should be undefined if target is provided per component logic
expect(link).not.toHaveAttribute('rel');
});
});
138 changes: 138 additions & 0 deletions regression-tests/channelSettings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import theme from '../shared/theme';

// Mock Head to avoid Helmet runtime requirements in tests
jest.mock('../src/components/head', () => {
const React = require('react');
return function MockHead() {
return <div data-testid="mock-head" />;
};
});
// Simple mock store to satisfy Provider without external deps
const baseStore = () => {
const getState = () => ({});
const dispatch = () => {};
const subscribe = () => () => {};
return { getState, dispatch, subscribe };
};
// Import the unwrapped component to avoid Apollo HOC requirement
// Import the raw component class by bypassing the default composed export.
// We require the file and grab the class before compose(connect, withRouter, getChannelByMatch, viewNetworkHandler).
// eslint-disable-next-line import/no-commonjs
const ChannelSettingsModule = require('../src/views/channelSettings/index.js');
const UnwrappedChannelSettings =
ChannelSettingsModule.ChannelSettings || ChannelSettingsModule.default;

// Helper to render the connected + routed component
const renderWithProviders = (
ui,
{ route = '/community/channel/settings' } = {}
) => {
const store = { ...baseStore(), dispatch: () => {} };
return render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>
</ThemeProvider>
</Provider>
);
};

// Build a minimal channel shape matching component expectations
const baseChannel = {
id: 'channel-1',
name: 'General',
isArchived: false,
channelPermissions: { isModerator: false, isOwner: false },
community: {
slug: 'community',
name: 'Community',
communityPermissions: { isOwner: false, isModerator: false },
},
};

describe('ChannelSettings regression', () => {
it('renders LoadingView when isLoading', () => {
renderWithProviders(
<UnwrappedChannelSettings
data={{ channel: null }}
match={{ params: { communitySlug: 'community' } }}
location={{ pathname: '/community/channel/settings' }}
isLoading={true}
hasError={false}
/>
);

expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

it('renders ErrorView when not loading and no channel', () => {
renderWithProviders(
<UnwrappedChannelSettings
data={{ channel: null }}
match={{ params: { communitySlug: 'community' } }}
location={{ pathname: '/community/channel/settings' }}
isLoading={false}
hasError={true}
/>
);

// ErrorView heading text from src/components/viewError
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});

it('blocks unauthorized users with permission error and upsell', () => {
const channel = {
...baseChannel,
channelPermissions: { isModerator: false, isOwner: false },
community: {
...baseChannel.community,
communityPermissions: { isOwner: false, isModerator: false },
},
};

renderWithProviders(
<UnwrappedChannelSettings
data={{ channel }}
match={{ params: { communitySlug: 'community' } }}
location={{ pathname: '/community/channel/settings' }}
isLoading={false}
hasError={false}
/>
);

expect(
screen.getByText(/you don’t have permission to manage this channel\./i)
).toBeInTheDocument();
expect(
screen.getByText(/return to community settings/i)
).toBeInTheDocument();
});

it('renders Overview when user has permissions', () => {
const channel = {
...baseChannel,
channelPermissions: { isModerator: true, isOwner: false },
};

renderWithProviders(
<UnwrappedChannelSettings
data={{ channel }}
match={{ params: { communitySlug: 'community' } }}
location={{ pathname: '/community/channel/settings' }}
isLoading={false}
hasError={false}
/>
);

expect(screen.getByText(/general settings/i)).toBeInTheDocument();
// Avoid querying deeper Apollo-connected Overview internals; assert header and subheading presence
expect(
screen.getByText(/return to community settings/i)
).toBeInTheDocument();
});
});
Loading
Loading