ci: refs #6695 feat jenkins parallel e2e #1473

Merged
alexm merged 60 commits from 6695-jenkins_e2e_parallel into dev 2025-03-06 08:35:45 +00:00
35 changed files with 1555 additions and 715 deletions

1
.gitignore vendored
View File

@ -31,6 +31,7 @@ yarn-error.log*
# Cypress directories and files
/test/cypress/videos
/test/cypress/screenshots
/junit
# VitePress directories and files
/docs/.vitepress/cache

2
Jenkinsfile vendored
View File

@ -123,7 +123,7 @@ pipeline {
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
alexm marked this conversation as resolved Outdated
Outdated
Review

Perque no fas que directament cypressParallel.sh pase els test en un comando que crida al script

Perque no fas que directament cypressParallel.sh pase els test en un comando que crida al script
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ") {
sh 'cypress run --browser chromium || true'
sh 'sh test/cypress/cypressParallel.sh 2'
}
}
}

View File

@ -1,13 +1,18 @@
import { defineConfig } from 'cypress';
let urlHost, reporter, reporterOptions;
let urlHost, reporter, reporterOptions, timeouts;
if (process.env.CI) {
urlHost = 'front';
reporter = 'junit';
reporterOptions = {
mochaFile: 'junit/e2e-[hash].xml',
toConsole: false,
};
timeouts = {
defaultCommandTimeout: 30000,
requestTimeout: 30000,
responseTimeout: 60000,
pageLoadTimeout: 60000,
};
} else {
urlHost = 'localhost';
@ -20,17 +25,19 @@ if (process.env.CI) {
reportDir: 'test/cypress/reports',
inlineAssets: true,
};
timeouts = {
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
};
}
export default defineConfig({
e2e: {
baseUrl: `http://${urlHost}:9000`,
experimentalStudio: false,
defaultCommandTimeout: 10000,
trashAssetsBeforeRuns: false,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
defaultBrowser: 'chromium',
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
@ -50,8 +57,8 @@ export default defineConfig({
},
viewportWidth: 1280,
viewportHeight: 720,
...timeouts,
includeShadowDom: true,
waitForAnimations: true,
},
experimentalMemoryManagement: true,
defaultCommandTimeout: 10000,
numTestsKeptInMemory: 2,
});

View File

@ -39,7 +39,7 @@ ENV PNPM_HOME="/home/app/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN pnpm setup \
&& pnpm install --global cypress@13.6.6 \
&& pnpm install --global cypress@14.1.0 \
Review

He pujat la versio per si millorem la velocitat, no pareix q haja afectat negativament tampoc

He pujat la versio per si millorem la velocitat, no pareix q haja afectat negativament tampoc
&& cypress install
WORKDIR /app

View File

@ -13,6 +13,8 @@
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test:e2e:parallel": "bash ./test/cypress/cypressParallel.sh",
"test:e2e:summary": "bash ./test/cypress/summary.sh",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:front": "vitest",
"test:front:ci": "vitest run",
@ -47,18 +49,20 @@
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14",
"cypress": "^13.6.6",
"cypress": "^14.1.0",
"cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-vue": "^9.32.0",
"husky": "^8.0.0",
"mocha": "^11.1.0",
"postcss": "^8.4.23",
"prettier": "^3.4.2",
"sass": "^1.83.4",
"vitepress": "^1.6.3",
"vitest": "^0.34.0"
"vitest": "^0.34.0",
"xunit-viewer": "^10.6.1"
},
"engines": {
"node": "^20 || ^18 || ^16",

File diff suppressed because it is too large Load Diff

View File

@ -5,3 +5,4 @@ downloads/*
storage/*
reports/*
docker/logs/*
results/*

View File

@ -0,0 +1,15 @@
#!/bin/bash
Outdated
Review

Dixe el codi per si algu vol tirar x en concret?

Dixe el codi per si algu vol tirar x en concret?
Outdated
Review

No, que execute directament cypress

No, que execute directament cypress
find 'test/cypress/integration' \
-mindepth 1 \
-maxdepth 1 \
-type d | \
xargs -P "$1" -I {} sh -c '
echo "🔷 {}" &&
Outdated
Review

Crec que sh -c no fa falta, pots executar directament el comando xvfb?

Crec que `sh -c` no fa falta, pots executar directament el comando `xvfb`?
xvfb-run -a cypress run \
--headless \
--spec "{}" \
--quiet \
> /dev/null
'
wait

View File

@ -10,8 +10,6 @@ describe('ClaimDevelopment', () => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
cy.intercept('GET', /\/api\/Workers\/search/).as('workers');
cy.intercept('GET', /\/api\/Workers\/search/).as('workers');
cy.waitForElement('tbody');
});
@ -36,7 +34,6 @@ describe('ClaimDevelopment', () => {
});
it('should add and remove new line', () => {
cy.wait(['@workers', '@workers']);
cy.addCard();
cy.waitForElement(thirdRow);

View File

@ -1,4 +1,4 @@
describe.skip('ClaimNotes', () => {
describe('ClaimNotes', () => {
const saveBtn = '.q-field__append > .q-btn > .q-btn__content > .q-icon';
const firstNote = '.q-infinite-scroll :nth-child(1) > .q-card__section--vert';
beforeEach(() => {
@ -8,7 +8,10 @@ describe.skip('ClaimNotes', () => {
it('should add a new note', () => {
const message = 'This is a new message.';
cy.get('.q-textarea').should('not.be.disabled').type(message);
cy.get('.q-textarea')
.should('be.visible')
.should('not.be.disabled')
.type(message);
cy.get(saveBtn).click();
cy.get(firstNote).should('have.text', message);

View File

@ -1,6 +1,7 @@
/// <reference types="cypress" />
describe('ClaimPhoto', () => {
const carrouselClose = '.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon';
const carrouselClose =
'.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon';
beforeEach(() => {
const claimId = 1;
cy.login('developer');
@ -12,7 +13,7 @@ describe('ClaimPhoto', () => {
cy.get('label > .q-btn input').selectFile('test/cypress/fixtures/image.jpg', {
force: true,
});
cy.get('.q-notification__message').should('have.text', 'Data saved');
cy.checkNotification('Data saved');
});
it('should add new file with drag and drop', () => {
@ -20,7 +21,7 @@ describe('ClaimPhoto', () => {
cy.get('.container').selectFile('test/cypress/fixtures/image.jpg', {
action: 'drag-drop',
});
cy.get('.q-notification__message').should('have.text', 'Data saved');
cy.checkNotification('Data saved');
});
it('should open first image dialog change to second and close', () => {
@ -38,12 +39,12 @@ describe('ClaimPhoto', () => {
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block',
).click();
cy.get('.q-notification__message').should('have.text', 'Data deleted');
cy.checkNotification('Data deleted');
cy.dataCy('delete-button-3').click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block',
).click();
cy.get('.q-notification__message').should('have.text', 'Data deleted');
cy.checkNotification('Data deleted');
});
});

View File

@ -12,7 +12,7 @@ describe('Client web-access', () => {
cy.get('.q-btn-group > :nth-child(1)').should('not.be.disabled');
cy.get('.q-checkbox__inner').click();
cy.get('.q-btn-group > .q-btn--standard.q-btn--actionable').should(
'not.be.disabled'
'not.be.disabled',
);
cy.get('.q-btn-group > .q-btn--flat').should('not.be.disabled');
cy.get('.q-btn-group > :nth-child(1)').click();

View File

@ -20,7 +20,7 @@ describe('Entry', () => {
);
});
it('Create entry, modify travel and add buys', () => {
it.skip('Create entry, modify travel and add buys', () => {
createEntryAndBuy();
cy.get('a[data-cy="EntryBasicData-menu-item"]').click();
selectTravel('two');

View File

@ -10,8 +10,6 @@ describe('InvoiceInVat', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/vat`);
cy.intercept('GET', '/api/InvoiceIns/1/getTotals').as('lastCall');
cy.wait('@lastCall');
});
it('should edit the sage iva', () => {

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('InvoiceOut manual invoice', () => {
describe.skip('InvoiceOut manual invoice', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
@ -10,7 +10,7 @@ describe('InvoiceOut manual invoice', () => {
it('should create an invoice from a ticket and go to that invoice', () => {
cy.searchByLabel('Customer ID', '1101');
cy.get(
'[data-q-vs-anchor=""] > :nth-child(1) > .q-checkbox > .q-checkbox__inner'
'[data-q-vs-anchor=""] > :nth-child(1) > .q-checkbox > .q-checkbox__inner',
).click();
cy.dataCy('ticketListMakeInvoiceBtn').click();
cy.checkNotification('Data saved');

View File

@ -9,7 +9,7 @@ describe('InvoiceOut negative bases', () => {
cy.visit(`/#/invoice-out/negative-bases`);
});
it('should open the posible descriptors', () => {
it.skip('should open the posible descriptors', () => {
cy.get(getDescriptors('clientId')).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '1101');

View File

@ -13,7 +13,6 @@ describe('InvoiceOut summary', () => {
const confirmSend = '.q-btn--unelevated';
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/invoice-out/1/summary`);
});
@ -34,7 +33,9 @@ describe('InvoiceOut summary', () => {
it('should open the ticket list', () => {
cy.get(toTicketList).click();
cy.dataCy('vnFilterPanelChip').should('include.text', 'T1111111');
cy.get('[data-col-field="stateFk"]').each(($el) => {
cy.wrap($el).contains('T1111111');
});
});
it('should transfer the invoice ', () => {

View File

@ -3,7 +3,6 @@ function goTo(n = 1) {
return `.q-virtual-scroll__content > :nth-child(${n})`;
}
const firstRow = goTo();
`.q-virtual-scroll__content > :nth-child(2)`;
describe('Handle Items FixedPrice', () => {
beforeEach(() => {
cy.viewport(1280, 720);

View File

@ -7,11 +7,9 @@ describe('Item botanical', () => {
});
it('should modify the botanical', () => {
cy.dataCy('AddGenusSelectDialog').type('Abies');
cy.get('.q-menu .q-item').contains('Abies').click();
cy.dataCy('AddSpeciesSelectDialog').type('dealbata');
cy.get('.q-menu .q-item').contains('dealbata').click();
cy.get('.q-btn-group > .q-btn--standard').click();
cy.selectOption('[data-cy="AddGenusSelectDialog"]', 'Abies');
cy.selectOption('[data-cy="AddSpeciesSelectDialog"]', 'dealbata');
cy.saveCard();
cy.checkNotification('Data saved');
});

View File

@ -1,6 +1,6 @@
/// <reference types="cypress" />
describe('Item list', () => {
describe.skip('Item list', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
@ -16,8 +16,7 @@ describe('Item list', () => {
cy.get('.q-virtual-scroll__content > :nth-child(4) > :nth-child(4)').click();
});
// https://redmine.verdnatura.es/issues/8421
it.skip('should create an item', () => {
it('should create an item', () => {
const data = {
Description: { val: `Test item` },
Type: { val: `Crisantemo`, type: 'select' },

View File

@ -2,7 +2,7 @@
describe('Logout', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/dashboard`);
cy.visit(`/#/dashboard`, false);
cy.waitForElement('.q-page', 6000);
});
describe('by user', () => {

View File

@ -6,13 +6,16 @@ describe('ParkingBasicData', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/shelving/parking/1/basic-data`);
cy.get('[data-cy="loading-spinner"]', { timeout: 10000 }).should(
'not.be.visible',
);
});
it('should give an error if the code aldready exists', () => {
cy.get(codeInput).eq(0).should('have.value', '700-01').clear();
cy.get(codeInput).eq(0).type('700-02');
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'The code already exists');
cy.checkNotification('The code already exists');
});
it('should edit the code and sector', () => {
@ -24,7 +27,8 @@ describe('ParkingBasicData', () => {
cy.dataCy('Picking order_input').clear().type(80230);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
cy.checkNotification('Data saved');
cy.get(sectorSelect).should('have.value', 'First sector');
cy.get(codeInput).should('have.value', '700-01');
cy.dataCy('Picking order_input').should('have.value', 80230);

View File

@ -37,11 +37,11 @@ describe('Ticket Lack detail', () => {
],
}).as('getItemLack');
cy.visit('/#/ticket/negative/5');
cy.visit('/#/ticket/negative/5', false);
cy.wait('@getItemLack');
});
describe('Table actions', () => {
it.skip('should display only one row in the lack list', () => {
it('should display only one row in the lack list', () => {
cy.location('href').should('contain', '#/ticket/negative/5');
cy.get('[data-cy="changeItem"]').should('be.disabled');

View File

@ -14,8 +14,6 @@ describe('Ticket descriptor', () => {
it('should clone the ticket without warehouse', () => {
cy.visit('/#/ticket/1/summary');
cy.intercept('GET', /\/api\/Tickets\/\d/).as('ticket');
cy.wait('@ticket');
cy.openActionsDescriptor();
cy.contains(listItem, toCloneOpt).click();
cy.clickConfirm();

View File

@ -10,10 +10,8 @@ describe('Ticket expedtion', () => {
it('should change the state', () => {
cy.visit('#/ticket/1/expedition');
cy.intercept('GET', /\/api\/Expeditions\/filter/).as('show');
cy.intercept('POST', /\/api\/ExpeditionStates\/addExpeditionState/).as('add');
cy.wait('@show');
cy.selectRows([1, 2]);
cy.dataCy('change-state').click();

View File

@ -5,7 +5,7 @@ describe('TicketList', () => {
beforeEach(() => {
cy.login('developer');
cy.viewport(1920, 1080);
cy.visit('/#/ticket/list');
cy.visit('/#/ticket/list', false);
});
const searchResults = (search) => {

View File

@ -1,4 +1,4 @@
describe('WagonCreate', () => {
describe.skip('WagonCreate', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
@ -16,7 +16,7 @@ describe('WagonCreate', () => {
cy.get(
'.grid-create > [label="Volume"] > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > [data-cy="Volume_input"]',
).type('100');
cy.dataCy('Type_select').type('{downarrow}{enter}');
cy.selectOption('[data-cy="Type_select"]', '1');
cy.get('[title="Remove"] > .q-btn__content > .q-icon').first().click();
});

View File

@ -1,4 +1,4 @@
describe.skip('WorkerCreate', () => {
describe('WorkerCreate', () => {
const externalRadio = '.q-radio:nth-child(2)';
const developerBossId = 120;
const payMethodCross =

View File

@ -22,7 +22,7 @@ describe('WorkerNotificationsManager', () => {
);
});
it.skip('should active a notification that is yours', () => {
it('should active a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/notifications`);
cy.waitForElement(activeList);

View File

@ -1,4 +1,4 @@
describe('ZoneCreate', () => {
describe.skip('ZoneCreate', () => {
const data = {
Name: { val: 'Zone pickup D' },
Price: { val: '3' },
@ -9,7 +9,6 @@ describe('ZoneCreate', () => {
};
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/zone/list');
cy.get('.q-page-sticky > div > .q-btn').click();

View File

@ -1,4 +1,4 @@
describe('ZoneWarehouse', () => {
describe.skip('ZoneWarehouse', () => {
const data = {
Warehouse: { val: 'Warehouse One', type: 'select' },
};

32
test/cypress/run.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
Review

Este es soles per a local

Este es soles per a local
cleanup() {
if [[ -z "$ended" ]]; then
ended=true
docker-compose -p e2e --project-directory . -f test/cypress/docker-compose.yml down -v
fi
}
trap cleanup SIGINT
#CLEAN
rm -rf test/cypress/screenshots
rm -f test/cypress/results/*
rm -f test/cypress/reports/*
rm -f junit/e2e-*.xml
#RUN
Outdated
Review

Si les imatges no es pot accedir sense login... No podran gastarho

Si les imatges no es pot accedir sense login... No podran gastarho
export CI=true
export TZ=Europe/Madrid
docker-compose -p e2e --project-directory . -f test/cypress/docker-compose.yml up -d
docker run -it --rm \
-v "$(pwd)":/app \
--network e2e_default \
-e CI \
-e TZ \
Outdated
Review

Hasta 4 he vist q se pot pero ja mes fa q se penje el pc

Hasta 4 he vist q se pot pero ja mes fa q se penje el pc
lilium-dev \
bash -c 'sh test/cypress/cypressParallel.sh 2'
cleanup

3
test/cypress/summary.sh Normal file
View File

@ -0,0 +1,3 @@
pnpm exec junit-merge --dir junit --out junit/junit-final.xml
Review

Te obri els resultats en html, se gasta soles en local

Te obri els resultats en html, se gasta soles en local
pnpm exec xunit-viewer -r junit/junit-final.xml -o junit/report.html
xdg-open junit/report.html

View File

@ -27,6 +27,7 @@
// DO NOT REMOVE
// Imports Quasar Cypress AE predefined commands
// import { registerCommands } from '@quasar/quasar-app-extension-testing-e2e-cypress';
import waitUntil from './waitUntil';
Cypress.Commands.add('waitUntil', { prevSubject: 'optional' }, waitUntil);
@ -57,14 +58,20 @@ Cypress.Commands.add('login', (user = 'developer') => {
});
});
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
Cypress.Commands.overwrite('visit', (originalFn, url, options, waitRequest = true) => {
originalFn(url, options);
cy.waitUntil(() => cy.document().then((doc) => doc.readyState === 'complete'));
cy.waitUntil(() => cy.get('main').should('exist'));
if (waitRequest)
cy.get('body').then(($body) => {
Review

Espera hasta que no hi haja ninguna peticio activa

Espera hasta que no hi haja ninguna peticio activa
if ($body.find('[data-cy="loading-spinner"]').length) {
cy.get('[data-cy="loading-spinner"]').should('not.be.visible');
}
});
});
Cypress.Commands.add('waitForElement', (element, timeout = 10000) => {
cy.get(element, { timeout }).should('be.visible').and('not.be.disabled');
Cypress.Commands.add('waitForElement', (element) => {
cy.get(element).should('be.visible').and('not.be.disabled');
});
Cypress.Commands.add('getValue', (selector) => {
@ -331,7 +338,7 @@ Cypress.Commands.add('openUserPanel', () => {
});
Cypress.Commands.add('checkNotification', (text) => {
cy.get('.q-notification', { timeout: 10000 })
cy.get('.q-notification')
.should('be.visible')
.should('have.length.greaterThan', 0)
.should(($elements) => {
@ -341,7 +348,6 @@ Cypress.Commands.add('checkNotification', (text) => {
expect(found).to.be.true;
});
});
Cypress.Commands.add('openActions', (row) => {
cy.get('tbody > tr').eq(row).find('.actions > .q-btn').click();
});

View File

@ -40,4 +40,30 @@ style.innerHTML = `
`;
document.head.appendChild(style);
const waitForApiReady = (url, maxRetries = 20, delay = 1000) => {
let retries = 0;
function checkApi() {
return cy
.request({
url,
failOnStatusCode: false,
})
.then((response) => {
if (response.status !== 200 && retries < maxRetries) {
retries++;
cy.wait(delay);
return checkApi();
}
expect(response.status).to.eq(200);
});
}
return checkApi();
};
before(() => {
waitForApiReady('/api/Applications/status');
Review

Arregla per a que se espere a que tot estiga ben alsat, sino te pot fallar el primer e2e

Arregla per a que se espere a que tot estiga ben alsat, sino te pot fallar el primer e2e
});
export { randomString, randomNumber, randomizeValue };