Skip to content

Commit

Permalink
feat: improve publication page (#1238)
Browse files Browse the repository at this point in the history
- feat: add component test to the project
- feat: update notifier to enable or not the notifications
  • Loading branch information
ReidyT authored Jun 7, 2024
1 parent fcba8e6 commit 647d960
Show file tree
Hide file tree
Showing 87 changed files with 5,274 additions and 1,545 deletions.
27 changes: 25 additions & 2 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ jobs:
VITE_SHOW_NOTIFICATIONS: ${{ vars.VITE_SHOW_NOTIFICATIONS }}
VITE_GRAASP_REDIRECTION_HOST: ${{ vars.VITE_GRAASP_REDIRECTION_HOST }}

# use the Cypress GitHub Action to run Cypress tests within the chrome browser
- name: Cypress run
# use the Cypress GitHub Action to run Cypress Component tests within the chrome browser
- name: Cypress run components
uses: cypress-io/github-action@v6
with:
install: false
component: true
# we launch the app in preview mode to avoid issues with hmr websockets from vite polluting the mocks
start: yarn preview:test
browser: chrome
Expand All @@ -70,6 +71,28 @@ jobs:
VITE_SHOW_NOTIFICATIONS: ${{ vars.VITE_SHOW_NOTIFICATIONS }}
VITE_GRAASP_REDIRECTION_HOST: ${{ vars.VITE_GRAASP_REDIRECTION_HOST }}

# use the Cypress GitHub Action to run Cypress E2E tests within the chrome browser
- name: Cypress run
uses: cypress-io/github-action@v6
with:
install: false
# the app preview is already run for the component tests
browser: chrome
quiet: true
config-file: cypress.config.ts
cache-key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
env:
VITE_PORT: ${{ vars.VITE_PORT }}
VITE_VERSION: ${{ vars.VITE_VERSION }}
VITE_GRAASP_DOMAIN: ${{ vars.VITE_GRAASP_DOMAIN }}
VITE_GRAASP_API_HOST: ${{ vars.VITE_GRAASP_API_HOST }}
VITE_GRAASP_AUTH_HOST: ${{ vars.VITE_GRAASP_AUTH_HOST }}
VITE_GRAASP_PLAYER_HOST: ${{ vars.VITE_GRAASP_PLAYER_HOST }}
VITE_GRAASP_LIBRARY_HOST: ${{ vars.VITE_GRAASP_LIBRARY_HOST }}
VITE_GRAASP_ANALYZER_HOST: ${{ vars.VITE_GRAASP_ANALYZER_HOST }}
VITE_SHOW_NOTIFICATIONS: ${{ vars.VITE_SHOW_NOTIFICATIONS }}
VITE_GRAASP_REDIRECTION_HOST: ${{ vars.VITE_GRAASP_REDIRECTION_HOST }}

# after the test run completes
# store any screenshots
# NOTE: screenshots will be generated only if E2E test failed
Expand Down
31 changes: 20 additions & 11 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import setupCoverage from '@cypress/code-coverage/task';
import { defineConfig } from 'cypress';

const ENV = {
VITE_GRAASP_REDIRECTION_HOST: process.env.VITE_GRAASP_REDIRECTION_HOST,
VITE_GRAASP_DOMAIN: process.env.VITE_GRAASP_DOMAIN,
VITE_GRAASP_API_HOST: process.env.VITE_GRAASP_API_HOST,
VITE_SHOW_NOTIFICATIONS: false,
VITE_GRAASP_AUTH_HOST: process.env.VITE_GRAASP_AUTH_HOST,
VITE_GRAASP_PLAYER_HOST: process.env.VITE_GRAASP_PLAYER_HOST,
VITE_GRAASP_ANALYZER_HOST: process.env.VITE_GRAASP_ANALYZER_HOST,
VITE_GRAASP_LIBRARY_HOST: process.env.VITE_GRAASP_LIBRARY_HOST,
VITE_GRAASP_ACCOUNT_HOST: process.env.VITE_GRAASP_ACCOUNT_HOST,
};

export default defineConfig({
video: false,
retries: {
runMode: 2,
},
chromeWebSecurity: false,
e2e: {
env: {
VITE_GRAASP_REDIRECTION_HOST: process.env.VITE_GRAASP_REDIRECTION_HOST,
VITE_GRAASP_DOMAIN: process.env.VITE_GRAASP_DOMAIN,
VITE_GRAASP_API_HOST: process.env.VITE_GRAASP_API_HOST,
VITE_SHOW_NOTIFICATIONS: false,
VITE_GRAASP_AUTH_HOST: process.env.VITE_GRAASP_AUTH_HOST,
VITE_GRAASP_PLAYER_HOST: process.env.VITE_GRAASP_PLAYER_HOST,
VITE_GRAASP_ANALYZER_HOST: process.env.VITE_GRAASP_ANALYZER_HOST,
VITE_GRAASP_LIBRARY_HOST: process.env.VITE_GRAASP_LIBRARY_HOST,
VITE_GRAASP_ACCOUNT_HOST: process.env.VITE_GRAASP_ACCOUNT_HOST,
},
env: ENV,
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
Expand All @@ -27,4 +29,11 @@ export default defineConfig({
},
baseUrl: `http://localhost:${process.env.VITE_PORT || 3333}`,
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
env: ENV,
},
});
81 changes: 81 additions & 0 deletions cypress/components/common/DebouncedTextField.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import DebouncedTextField, {
DEBOUNCE_MS,
} from '@/components/input/DebouncedTextField';
import { DEBOUNCED_TEXT_FIELD_ID } from '@/config/selectors';

const ON_UPDATE_SPY = 'onUpdate';
const getSpyOnUpdate = () => `@${ON_UPDATE_SPY}`;
const eventHandler = {
onUpdate: (_newValue: string) => {},
};
const LABEL = 'Label';
const VALUE = 'My value';

const getTextArea = () => cy.get(`#${DEBOUNCED_TEXT_FIELD_ID}`);

describe('<DebouncedTextField />', () => {
beforeEach(() => {
cy.spy(eventHandler, 'onUpdate').as(ON_UPDATE_SPY);
});

describe('Value is defined', () => {
beforeEach(() => {
cy.mount(
<DebouncedTextField
initialValue={VALUE}
label={LABEL}
onUpdate={eventHandler.onUpdate}
/>,
);
});
it('Initial value should not called onUpdate', () => {
getTextArea().should('be.visible');
cy.get(getSpyOnUpdate()).should('not.be.called');
});

it('Edit value should be called onUpdate', () => {
const NEW_VALUE = 'My new value';
getTextArea().clear().type(NEW_VALUE);

cy.get(getSpyOnUpdate()).should('be.calledOnceWith', NEW_VALUE);
});

it('Edit value multiple times should debounce onUpdate', () => {
const NEW_VALUE = 'My new value';
getTextArea().clear().type(NEW_VALUE);

// Write again before the end of the debounce timeout
const APPEND_VALUE = ' which has been debounced';
cy.wait(DEBOUNCE_MS - 100);
getTextArea().type(APPEND_VALUE);
cy.get(getSpyOnUpdate()).should(
'be.calledOnceWith',
`${NEW_VALUE}${APPEND_VALUE}`,
);
});
});

describe('Can not be empty if not allowed', () => {
beforeEach(() => {
cy.mount(
<DebouncedTextField
initialValue={VALUE}
label={LABEL}
onUpdate={eventHandler.onUpdate}
required
/>,
);
});

it('Empty value should not call onUpdate if not allowed', () => {
getTextArea().clear();
cy.get(getSpyOnUpdate()).should('not.be.called');
});

it('onUpdate should be called if value is not empty', () => {
const NEW_VALUE = 'My new value';
getTextArea().clear().type(NEW_VALUE);
cy.get(getSpyOnUpdate()).should('be.calledOnceWith', NEW_VALUE);
});
});
});
145 changes: 145 additions & 0 deletions cypress/components/common/MultiSelectChipInput.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import MultiSelectChipInput from '@/components/input/MultiSelectChipInput';
import {
MULTI_SELECT_CHIP_ADD_BUTTON_ID,
MULTI_SELECT_CHIP_CONTAINER_ID,
MULTI_SELECT_CHIP_INPUT_ID,
buildDataCyWrapper,
buildMultiSelectChipsSelector,
} from '@/config/selectors';

const ON_SAVE_SPY = 'onSave';
const getSpyOnSave = () => `@${ON_SAVE_SPY}`;
const eventHandler = { onSave: (_values: string[]) => {} };
const EXISTING_VALUES = ['first', 'second', 'third'];
const NEW_VALUE = 'my new value';
const LABEL = 'my label';

const getInput = () =>
cy.get(`${buildDataCyWrapper(MULTI_SELECT_CHIP_INPUT_ID)} input`);

const addANewValue = (newValue: string) => {
getInput().type(newValue);
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).click();
};

const removeAValue = (valueToRemove: string) => {
const idxOfRemovedValue = EXISTING_VALUES.findIndex(
(value) => value === valueToRemove,
);

if (idxOfRemovedValue === -1) {
throw new Error(`Given value to remove "${valueToRemove}" was not found!`);
}

cy.get(
`${buildDataCyWrapper(buildMultiSelectChipsSelector(idxOfRemovedValue))} svg`,
).click();
};

describe('<MultiSelectChipInput />', () => {
beforeEach(() => {
cy.spy(eventHandler, 'onSave').as(ON_SAVE_SPY);
});

describe('Data is empty', () => {
beforeEach(() => {
cy.mount(
<MultiSelectChipInput
data={[]}
onSave={eventHandler.onSave}
label={LABEL}
/>,
);
});

it('Chips container should not exist when no data', () => {
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID)).should(
'not.exist',
);
});

it('Add a new value should add a new chip', () => {
addANewValue(NEW_VALUE);
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
.children()
.should('have.length', 1)
.should('contain', NEW_VALUE);
});

it('Add a new value should reset current value', () => {
addANewValue(NEW_VALUE);
getInput().should('have.value', '');
});

it('Add a new empty value should not be possible', () => {
getInput().should('have.value', '');
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).should(
'be.disabled',
);
});
});

describe('Have some data', () => {
const valueToRemove = EXISTING_VALUES[1];

beforeEach(() => {
cy.mount(
<MultiSelectChipInput
onSave={eventHandler.onSave}
data={EXISTING_VALUES}
label={LABEL}
/>,
);
});

it('Chips container should contains existing chips', () => {
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
.children()
.should('have.length', EXISTING_VALUES.length);
});

it('Add a new value should add a new chip', () => {
addANewValue(NEW_VALUE);
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
.children()
.should('have.length', EXISTING_VALUES.length + 1)
.should('contain', NEW_VALUE);
});

it('Add a new value should call onSave', () => {
addANewValue(NEW_VALUE);
cy.get(getSpyOnSave()).should('be.calledWith', [
...EXISTING_VALUES,
NEW_VALUE,
]);
});

it('Add an existing value should not be possible', () => {
getInput().type(EXISTING_VALUES[0].toUpperCase());
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).should(
'be.disabled',
);
});

it('Add an existing value should not call onSave', () => {
getInput().type(EXISTING_VALUES[0].toUpperCase());
cy.get(getSpyOnSave()).should('not.be.called');
});

it('Remove a value should remove the chip', () => {
removeAValue(valueToRemove);
cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
.children()
.should('have.length', EXISTING_VALUES.length - 1)
.should('not.contain', valueToRemove);
});

it('Remove a value should call onSave', () => {
removeAValue(valueToRemove);
cy.get(getSpyOnSave()).should(
'be.calledWith',
EXISTING_VALUES.filter((e) => e !== valueToRemove),
);
});
});
});
Loading

0 comments on commit 647d960

Please sign in to comment.