Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
14 changes: 8 additions & 6 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"extends": [
"airbnb",
"prettier",
"prettier",
"jest-enzyme",
"plugin:jest/recommended"
"plugin:jest/recommended",
"plugin:testing-library/react",
"plugin:jest-dom/recommended"
],
"parser": "@babel/eslint-parser",
"plugins": ["prettier", "jest"],
"plugins": ["prettier", "jest", "testing-library", "jest-dom"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
Expand All @@ -19,7 +19,8 @@
"env": {
"es6": true,
"browser": true,
"node": true
"node": true,
"jest": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
Expand All @@ -46,6 +47,7 @@
"browser-test/utils.js"
]
}
]
],
"camelcase": "off"
}
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
yarn install
yarn setup
sudo apt update -qq
sudo apt install -qq -y libssl-dev libffi-dev
sudo apt install -qq -y libssl-dev libffi-dev expect

- name: Set up Python
uses: actions/setup-python@v6
Expand Down
4 changes: 2 additions & 2 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ module.exports = {
],
plugins: [
["@babel/plugin-transform-runtime"],
["@babel/plugin-proposal-class-properties", {loose: true}],
["@babel/plugin-transform-class-properties", {loose: true}],
["@babel/plugin-transform-arrow-functions"],
["@babel/plugin-syntax-dynamic-import"],
["@babel/plugin-proposal-private-property-in-object", {loose: true}],
["@babel/plugin-transform-private-methods", {loose: true}],
["@babel/plugin-transform-spread"],
"transform-remove-strict-mode",
],
Expand Down
1 change: 1 addition & 0 deletions browser-test/create-mobile-configuration.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ try {
fs.writeFileSync(configPath, yaml.dump(content));
fs.writeFileSync(path.join(jsDir, data.allOrgScript), "");
} catch (err) {
/* eslint-disable-next-line no-console */
console.log(err);
}
2 changes: 1 addition & 1 deletion browser-test/initialize_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def load_test_data():
)
RegisteredUser.objects.create(user=user, method=data["method"])
OrganizationUser.objects.create(organization=org, user=user)

sys.exit(0)

try:
org = Organization.objects.get(slug=test_user_organization)
Expand Down
41 changes: 37 additions & 4 deletions browser-test/js-file-inject.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,58 @@ describe("Selenium tests to check JS file injection in organization page", () =>
it("should load js file for entire application", async () => {
const jsFile = initialData().allOrgScript;
await driver.get(urls.login);

// Wait for page to be fully loaded and stable
await driver.sleep(1000);

let scriptSources = [];
// Re-fetch scripts to avoid stale element reference
let scripts = await getElementsByCss(driver, "script");
await Promise.all(

// Get all script sources in a single pass to avoid iterating over potentially stale elements
scriptSources = await Promise.all(
scripts.map(async (script) => {
scriptSources.push(await script.getAttribute("src"));
try {
return await script.getAttribute("src");
} catch (error) {
// If element becomes stale, return null and filter it out
return null;
}
}),
);

// Filter out null values from stale elements
scriptSources = scriptSources.filter((src) => src !== null);

expect(scriptSources.includes(`http://127.0.0.1:8080/${jsFile}`)).toEqual(
true,
);

const data = initialData().mobileVerificationTestUser;
await driver.get(urls.verificationLogin(data.organization));

// Wait for page to be fully loaded and stable after navigation
await driver.sleep(1000);

scriptSources = [];
// Re-fetch scripts after navigation to get fresh element references
scripts = await getElementsByCss(driver, "script");
await Promise.all(

// Get all script sources in a single pass
scriptSources = await Promise.all(
scripts.map(async (script) => {
scriptSources.push(await script.getAttribute("src"));
try {
return await script.getAttribute("src");
} catch (error) {
// If element becomes stale, return null and filter it out
return null;
}
}),
);

// Filter out null values from stale elements
scriptSources = scriptSources.filter((src) => src !== null);

expect(scriptSources.includes(`http://127.0.0.1:8080/${jsFile}`)).toEqual(
true,
);
Expand Down
2 changes: 1 addition & 1 deletion browser-test/mobile-phone-change.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe("Selenium tests for <MobilePhoneChange />", () => {
submitBtn = await getElementByCss(driver, "input[type='submit']");
await driver.wait(until.elementIsVisible(submitBtn));
submitBtn.click();
successToastDiv = await getElementByCss(driver, "div[role=alert]");
successToastDiv = await getElementByCss(driver, successToastSelector);
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual(
"SMS verification code sent successfully.",
Expand Down
2 changes: 2 additions & 0 deletions browser-test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const getElementByCss = async (driver, css) => {
try {
el = await driver.wait(until.elementLocated(By.css(css)), waitTime);
} catch (err) {
/* eslint-disable-next-line no-console */
console.log(err, css, await driver.getPageSource());
}
return el;
Expand All @@ -42,6 +43,7 @@ export const getElementsByCss = async (driver, css) => {
try {
el = await driver.wait(until.elementsLocated(By.css(css)), waitTime);
} catch (err) {
/* eslint-disable-next-line no-console */
console.log(err, css, await driver.getPageSource());
}
return el;
Expand Down
42 changes: 28 additions & 14 deletions client/app.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
/* eslint-disable import/no-import-module-exports */
import "./index.css";

import {createRoot} from "react-dom/client";
import {Provider, connect} from "react-redux";

import {HelmetProvider} from "react-helmet-async";
import {CookiesProvider} from "react-cookie";
import PropTypes from "prop-types";
import React from "react";
import {Route, BrowserRouter as Router, Routes} from "react-router-dom";
import {render} from "react-dom";
import {ToastContainer} from "react-toastify";
import OrganizationRoutes from "./routes";
import organizations from "./organizations.json";
import history from "./utils/history";
import parseOrganizations from "./actions/parse-organizations";
import store from "./store";
import isOldBrowser from "./utils/is-old-browser";
Expand All @@ -25,7 +23,7 @@ class BaseApp extends React.Component {

render() {
return (
<Router history={history}>
<Router>
<ToastContainer className={isOldBrowser() ? "oldbrowser" : null} />
<Routes>
<Route path="*" element={<OrganizationRoutes />} />
Expand All @@ -47,16 +45,30 @@ const mapDispatchToProps = (dispatch) => ({

const App = connect(null, mapDispatchToProps)(BaseApp);

export default function app() {
render(
<CookiesProvider>
<Provider store={store}>
<App />
</Provider>
</CookiesProvider>,
document.getElementById("root"),
let root = null;

const app = () => {
const container = document.getElementById("root");
if (!container) {
throw new Error(
"Root element not found. Ensure an element with id='root' exists in the HTML.",
);
}

if (root === null) {
root = createRoot(container);
}

root.render(
<HelmetProvider>
<CookiesProvider>
<Provider store={store}>
<App />
</Provider>
</CookiesProvider>
</HelmetProvider>,
);
}
};

if (module && module.hot) {
module.hot.accept();
Expand All @@ -65,3 +77,5 @@ if (module && module.hot) {
window.addEventListener("load", () => {
document.getElementById("preload").remove();
});

export default app;
14 changes: 12 additions & 2 deletions client/components/404/404.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable react/require-default-props */
import "./index.css";

import PropTypes from "prop-types";
Expand All @@ -16,7 +15,11 @@ export default class DoesNotExist extends React.Component {
render() {
const {orgSlug, page} = this.props;
return (
<div className="container content" id="not-foud-404">
<div
className="container content"
id="not-found-404"
data-testid="not-found-404"
>
<div className="inner">
<div className="main-column">
<div className="inner">
Expand Down Expand Up @@ -56,3 +59,10 @@ DoesNotExist.propTypes = {
orgName: PropTypes.string,
setTitle: PropTypes.func,
};

DoesNotExist.defaultProps = {
page: undefined,
orgSlug: "",
orgName: undefined,
setTitle: () => {},
};
31 changes: 16 additions & 15 deletions client/components/404/404.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import ShallowRenderer from "react-test-renderer/shallow";
import {shallow} from "enzyme";
import {render} from "@testing-library/react";
import "@testing-library/jest-dom";
import {TestRouter} from "../../test-utils";
import getConfig from "../../utils/get-config";
import loadTranslation from "../../utils/load-translation";
import DoesNotExist from "./404";
Expand All @@ -17,12 +18,14 @@ const createTestProps = (props) => ({
...props,
});

const renderWithRouter = (component) =>
render(<TestRouter>{component}</TestRouter>);

describe("<DoesNotExist /> rendering with placeholder translation tags", () => {
const props = createTestProps();
it("should render translation placeholder correctly", () => {
const renderer = new ShallowRenderer();
const wrapper = renderer.render(<DoesNotExist {...props} />);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithRouter(<DoesNotExist {...props} />);
expect(container).toMatchSnapshot();
});
});

Expand All @@ -32,31 +35,29 @@ describe("<DoesNotExist /> rendering", () => {
});

it("should render correctly default 404 page without props", () => {
const renderer = new ShallowRenderer();
const component = renderer.render(<DoesNotExist />);
expect(component).toMatchSnapshot();
const {container} = renderWithRouter(<DoesNotExist />);
expect(container).toMatchSnapshot();
});

it("should render correctly custom 404 page with props", () => {
const props = createTestProps();
const renderer = new ShallowRenderer();
const component = renderer.render(<DoesNotExist {...props} />);
expect(component).toMatchSnapshot();
const {container} = renderWithRouter(<DoesNotExist {...props} />);
expect(container).toMatchSnapshot();
});

it("should set title with organisation name", () => {
const props = createTestProps();
const wrapper = shallow(<DoesNotExist {...props} />);
const setTitleMock = wrapper.instance().props.setTitle.mock;
renderWithRouter(<DoesNotExist {...props} />);
const setTitleMock = props.setTitle.mock;
expect(setTitleMock.calls.pop()).toEqual(["404 Not found", props.orgName]);
});

it("should not call setTitle if organization is undefined", () => {
const props = createTestProps();
props.page = undefined;
props.orgName = undefined;
const wrapper = shallow(<DoesNotExist {...props} />);
const setTitleMock = wrapper.instance().props.setTitle.mock;
renderWithRouter(<DoesNotExist {...props} />);
const setTitleMock = props.setTitle.mock;
expect(setTitleMock.calls.length).toBe(0);
});
});
Loading
Loading