diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..abd534e --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,31 @@ +name: Playwright Tests + +on: + pull_request: + branches: [main] # Corre cuando el PR apunta a main + push: + branches: [draft/playwright-wip] # Opcional: corre en cada push a tu rama de trabajo + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + + - run: npm ci + - run: npx playwright install --with-deps + - run: npx playwright test --reporter=html + + - name: Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report + retention-days: 7 diff --git a/api.config.ts b/api.config.ts new file mode 100644 index 0000000..67852b0 --- /dev/null +++ b/api.config.ts @@ -0,0 +1,40 @@ +import { defineConfig, devices } from "@playwright/test"; +import "dotenv/config"; +const isCI = !!process.env.CI; + +export default defineConfig({ + testDir: "./src/test/API", + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: isCI + ? [["html", { open: "never" }], ["list"]] + : [["html", { open: "never" }]], + + expect: { + timeout: 5_000, + }, + + use: { + baseURL: process.env.BASE_URL || "https://automationintesting.online", + headless: true, + actionTimeout: 0, + navigationTimeout: 30_000, + trace: "on-first-retry", + video: "retain-on-failure", + screenshot: "only-on-failure", + serviceWorkers: "block", + }, + + projects: [ + // Desktop + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + + // Mobile emulation + { name: "Mobile Chrome", use: { ...devices["Pixel 7"] } }, + { name: "Mobile Safari", use: { ...devices["iPhone 14"] } }, + ], +}); diff --git a/nonfunctional/performance/api_k6/env_local.bat b/nonfunctional/performance/api_k6/env_local.bat new file mode 100644 index 0000000..33b11a8 --- /dev/null +++ b/nonfunctional/performance/api_k6/env_local.bat @@ -0,0 +1,10 @@ +@echo off +set API_BOOKER_BAT_URL=https://restful-booker.herokuapp.com +set API_BOOKER_BAT_USERNAME=admin +set API_BOOKER_BAT_PASSWORD=password123 +k6 run smoke_test.js +pause + +echo URL=%API_BOOKER_BAT_URL% +echo USER=%API_BOOKER_BAT_USERNAME% +echo PASS=%API_BOOKER_BAT_PASSWORD% diff --git a/nonfunctional/performance/api_k6/load_test.js b/nonfunctional/performance/api_k6/load_test.js new file mode 100644 index 0000000..660e83d --- /dev/null +++ b/nonfunctional/performance/api_k6/load_test.js @@ -0,0 +1,163 @@ +// init context: importing modules +import http from 'k6/http'; +import { sleep } from 'k6'; //sleep lo usamos solo si quieres simular respiritos entre pasos +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; +import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'; + + +// init context: define k6 options +export const options = { + scenarios: { + load_e2e_booking: { + executor: 'constant-arrival-rate', + rate: 3, // 3 solicitudes por segundo (conservador) + timeUnit: '1s', + duration: '12m', // sostén + preAllocatedVUs: 10, // VUs que K6 reserva para cumplir la tasa + maxVUs: 20, // tope por si hace falta más + exec: 'default', + tags: { test_type: 'load', flow: 'e2e_booking' }, + }, + warmup: { + executor: 'ramping-arrival-rate', + startRate: 1, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + stages: [ + { duration: '2m', target: 3 }, // sube de 1 → 3 req/s + ], + exec: 'default', + tags: { test_type: 'warmup', flow: 'e2e_booking' }, + }, + }, + thresholds: { + checks: ['rate>0.99'], + http_req_duration: ['p(95)<800'], + http_req_failed: ['rate<0.01'], + }, +}; + +// 1. init code - La idea: en setup() obtienes el token una sola vez y devuelves todo lo que vas a reutilizar en default(). +export function setup() { + // A) Variables de entorno + const baseurl = __ENV.API_BOOKER_BAT_URL; + const username = __ENV.API_BOOKER_BAT_USERNAME; + const password = __ENV.API_BOOKER_BAT_PASSWORD; + + if (!baseurl || !username || !password) { + throw new Error('Faltan variables de entorno: API_BOOKER_BAT_URL, API_BOOKER_BAT_USERNAME, API_BOOKER_BAT_PASSWORD'); + } + + // B) Headers JSON comunes + const jsonheaders = { + 'Content-Type': 'application/json', + }; + + // C) Login y obtención del token + const createtoken = http.post(`${baseurl}/auth`, // Llama al endpoint con verbo POST y crea una URL de manera dinámica + JSON.stringify({ username: username, password: password }), // Cuerpo de la petición, convierte tu objeto JS a texto plano JSON (el formato que espera la API) + { headers: jsonheaders } // Envia encabezados (headers), aquí el Content-Type + ); + const token = createtoken.json('token'); // Extrae el token de la respuesta JSON + + return { + token, + baseurl + }; + +} +// 2. setup code +export default function (data) { + + //Post - Crea un booking + const baseurl = __ENV.API_BOOKER_BAT_URL; + const baseurlpost =`${baseurl}/booking`; + const jsonpostheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + const payload = JSON.stringify({ + firstname: `smoke_${Date.now()}`, + lastname: 'Brown', + totalprice: 111, + depositpaid: true, + bookingdates: { checkin: '2018-01-01', checkout: '2019-01-01' }, + additionalneeds: 'Breakfast', + }); + + const createbooking = http.post(baseurlpost, payload, { headers: jsonpostheaders }); + const bookingidnew = createbooking.json('bookingid'); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + // PATCH - Actualiza el nombre del booking creado + const baseurlpatch =`${baseurl}/booking/${bookingidnew}`; + const tokenpatch = data.token; // Obtén el token del objeto data pasado desde setup() + const jsonpatchheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenpatch}`, // Usa el token obtenido en setup() + }; + const payloadpatch = JSON.stringify({ + firstname: `smoke_${Date.now()}`, // Actualiza el nombre con un valor único + lastname: 'Fernandez', // Actualiza el apellido + }); + + const updatebooking = http.patch(baseurlpatch, payloadpatch, { headers: jsonpatchheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //PUT - Actualiza todo el booking creado + const baseurlput =`${baseurl}/booking/${bookingidnew}`; + const tokenput = data.token; // Obtén el token del objeto data pasado desde setup() + const payloadput = JSON.stringify({ + "firstname" : "James", + "lastname" : "Smith", + "totalprice" : 111, + "depositpaid" : true, + "bookingdates" : { + "checkin" : "2018-01-01", + "checkout" : "2019-01-01" + }, + "additionalneeds" : "Breakfast" + }); + const jsonputheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenput}`, // Usa el token obtenido en setup() + }; + + const putbooking = http.put(baseurlput, payloadput, { headers: jsonputheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //DELETE - Elimina el booking creado + const urldelete = `${baseurl}/booking/${bookingidnew}`; + const tokendelete = data.token; // Obtén el token del objeto data pasado desde setup() + const jsondeleteheaders = { + 'Accept': 'application/json', + 'Cookie': `token=${tokendelete}`, // Usa el token obtenido en setup() + }; + + const deletebooking = http.del(urldelete, null, { headers: jsondeleteheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //GET - Ping - HealthCheck + const urlgetping = `${baseurl}/ping`; + const getping = http.get(urlgetping); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + 'summary.json': JSON.stringify(data, null, 2), + 'summary.html': htmlReport(data), + }; +} + +export function teardown() { + console.log('Teardown: prueba finalizada.'); +} \ No newline at end of file diff --git a/nonfunctional/performance/api_k6/smoke_test.js b/nonfunctional/performance/api_k6/smoke_test.js new file mode 100644 index 0000000..89f20e4 --- /dev/null +++ b/nonfunctional/performance/api_k6/smoke_test.js @@ -0,0 +1,188 @@ +// init context: importing modules +import http from 'k6/http'; +import { sleep } from 'k6'; //sleep lo usamos solo si quieres simular respiritos entre pasos +import { check } from 'k6'; //check te permite afirmar “status 200”, “tiene id”, etc. +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; +import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'; + + +// init context: define k6 options +export const options = { + vus: 1, // 1 usuario + iterations: 1, // lo hace durante 45 segundos seguidos. + thresholds: { + checks: ['rate>0.99'], // significa que al menos el 99 % de los checks (validaciones) deben pasar; si no, la prueba falla. + http_req_duration: ['p(95)<800'], // significa que el 95 % de las peticiones HTTP deben responder en menos de 800 ms. + }, +}; + +// 1. init code - La idea: en setup() obtienes el token una sola vez y devuelves todo lo que vas a reutilizar en default(). +export function setup() { + // A) Variables de entorno + const baseurl = __ENV.API_BOOKER_BAT_URL; + const username = __ENV.API_BOOKER_BAT_USERNAME; + const password = __ENV.API_BOOKER_BAT_PASSWORD; + + if (!baseurl || !username || !password) { + throw new Error('Faltan variables de entorno: API_BOOKER_BAT_URL, API_BOOKER_BAT_USERNAME, API_BOOKER_BAT_PASSWORD'); + } + + // B) Headers JSON comunes + const jsonheaders = { + 'Content-Type': 'application/json', + }; + + // C) Login y obtención del token + const createtoken = http.post(`${baseurl}/auth`, // Llama al endpoint con verbo POST y crea una URL de manera dinámica + JSON.stringify({ username: username, password: password }), // Cuerpo de la petición, convierte tu objeto JS a texto plano JSON (el formato que espera la API) + { headers: jsonheaders } // Envia encabezados (headers), aquí el Content-Type + ); + check(createtoken, { // Check es como el “expect” de Playwright o el “assert” de Pytest || createtoken: es el objeto que devuelve http.post, http.get, etc. + 'login status 200': (r) => r.status === 200, // r: es la condición que debe cumplirse. + 'token present': (r) => r.json('token') !== '', // Léame el JSON de esta respuesta y deme el valor que tenga el campo 'token' || Asegúrese de que el token no sea una cadena vacía. + }); + + const token = createtoken.json('token'); // Extrae el token de la respuesta JSON + console.log(`Token obtenido: ${token}`); // Muestra el token en la consola de k6 + + return { + token, + baseurl + }; + +} +// 2. setup code +export default function (data) { + + //Post - Crea un booking + const baseurl = __ENV.API_BOOKER_BAT_URL; + const baseurlpost =`${baseurl}/booking`; + const jsonpostheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + const payload = JSON.stringify({ + firstname: `smoke_${Date.now()}`, + lastname: 'Brown', + totalprice: 111, + depositpaid: true, + bookingdates: { checkin: '2018-01-01', checkout: '2019-01-01' }, + additionalneeds: 'Breakfast', + }); + + const createbooking = http.post(baseurlpost, payload, { headers: jsonpostheaders }); + console.log(`Response body: ${createbooking.body}`); // Muestra el cuerpo de la respuesta en la consola de k6 + + check(createbooking, { + 'login status 200': (r) => r.status === 200, + 'bookingid present': (r) => r.json('bookingid') !== '', + }); + + const bookingidnew = createbooking.json('bookingid'); + console.log(`Bookingid obtenido: ${bookingidnew}`); + + + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + // PATCH - Actualiza el nombre del booking creado + const baseurlpatch =`${baseurl}/booking/${bookingidnew}`; + const tokenpatch = data.token; // Obtén el token del objeto data pasado desde setup() + const jsonpatchheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenpatch}`, // Usa el token obtenido en setup() + }; + const payloadpatch = JSON.stringify({ + firstname: `smoke_${Date.now()}`, // Actualiza el nombre con un valor único + lastname: 'Fernandez', // Actualiza el apellido + }); + + const updatebooking = http.patch(baseurlpatch, payloadpatch, { headers: jsonpatchheaders }); + console.log(`Response body: ${updatebooking.body}`); // Muestra el cuerpo de la respuesta en la consola de k6 + + check(updatebooking, { + 'update status 200': (r) => r.status === 200, + 'firstname updated': (r) => r.json('firstname') === updatebooking.json('firstname'), + 'lastname updated': (r) => r.json('lastname') === updatebooking.json('lastname'), + }); + + + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //PUT - Actualiza todo el booking creado + const baseurlput =`${baseurl}/booking/${bookingidnew}`; + const tokenput = data.token; // Obtén el token del objeto data pasado desde setup() + const payloadput = JSON.stringify({ + "firstname" : "James", + "lastname" : "Smith", + "totalprice" : 111, + "depositpaid" : true, + "bookingdates" : { + "checkin" : "2018-01-01", + "checkout" : "2019-01-01" + }, + "additionalneeds" : "Breakfast" + }); + const jsonputheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenput}`, // Usa el token obtenido en setup() + }; + + const putbooking = http.put(baseurlput, payloadput, { headers: jsonputheaders }); + console.log(`Response body: ${putbooking.body}`); // Muestra el cuerpo de la respuesta en la consola de k6 + + check(putbooking, { + 'put status 200': (r) => r.status === 200, + 'firstname updated': (r) => r.json('firstname') === 'James', + 'lastname updated': (r) => r.json('lastname') === 'Smith', + }); + + + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //DELETE - Elimina el booking creado + const urldelete = `${baseurl}/booking/${bookingidnew}`; + const tokendelete = data.token; // Obtén el token del objeto data pasado desde setup() + const jsondeleteheaders = { + 'Accept': 'application/json', + 'Cookie': `token=${tokendelete}`, // Usa el token obtenido en setup() + }; + + const deletebooking = http.del(urldelete, null, { headers: jsondeleteheaders }); + check(deletebooking, { + 'delete status 201': (r) => r.status === 201, + }); + + + console.log(`Status code: ${deletebooking.status}`); // Muestra el código de estado en la consola de k6 + console.log(`Response body: ${deletebooking.body}`); // Muestra el cuerpo de la respuesta en la consola de k6 + + + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //GET - Ping - HealthCheck + const urlgetping = `${baseurl}/ping`; + const getping = http.get(urlgetping); + check(getping, { + 'ping status 201': (r) => r.status === 201, + }); + console.log(`Status code: ${getping.status}`); + +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + 'summary.json': JSON.stringify(data, null, 2), + 'summary.html': htmlReport(data), + }; +} + +export function teardown() { + console.log('Teardown: prueba finalizada.'); +} \ No newline at end of file diff --git a/nonfunctional/performance/api_k6/soak_test.js b/nonfunctional/performance/api_k6/soak_test.js new file mode 100644 index 0000000..fdd95c6 --- /dev/null +++ b/nonfunctional/performance/api_k6/soak_test.js @@ -0,0 +1,152 @@ +// init context: importing modules +import http from 'k6/http'; +import { sleep } from 'k6'; //sleep lo usamos solo si quieres simular respiritos entre pasos +import { check } from 'k6'; //check te permite afirmar “status 200”, “tiene id”, etc. +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; +import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'; + + +// init context: define k6 options +export const options = { + scenarios: { + soak_e2e_booking: { + executor: 'constant-arrival-rate', + rate: 3, // 3 req/s (moderado y “amable”) + timeUnit: '1s', + duration: '45m', // 45–60 min es un buen inicio + preAllocatedVUs: 15, + maxVUs: 30, + exec: 'default', // o 'default' si tu flujo se llama default + tags: { test_type: 'soak', flow: 'default' }, + }, + }, + thresholds: { + checks: ['rate>0.99'], + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<900'], // un pelín más relajado que Load + }, +} + +// 1. init code - La idea: en setup() obtienes el token una sola vez y devuelves todo lo que vas a reutilizar en default(). +export function setup() { + // A) Variables de entorno + const baseurl = __ENV.API_BOOKER_BAT_URL; + const username = __ENV.API_BOOKER_BAT_USERNAME; + const password = __ENV.API_BOOKER_BAT_PASSWORD; + + if (!baseurl || !username || !password) { + throw new Error('Faltan variables de entorno: API_BOOKER_BAT_URL, API_BOOKER_BAT_USERNAME, API_BOOKER_BAT_PASSWORD'); + } + + // B) Headers JSON comunes + const jsonheaders = { + 'Content-Type': 'application/json', + }; + + // C) Login y obtención del token + const createtoken = http.post(`${baseurl}/auth`, // Llama al endpoint con verbo POST y crea una URL de manera dinámica + JSON.stringify({ username: username, password: password }), // Cuerpo de la petición, convierte tu objeto JS a texto plano JSON (el formato que espera la API) + { headers: jsonheaders } // Envia encabezados (headers), aquí el Content-Type + ); + const token = createtoken.json('token'); // Extrae el token de la respuesta JSON + + return { + token, + baseurl + }; + +} +// 2. setup code +export default function (data) { + + //Post - Crea un booking + const baseurl = __ENV.API_BOOKER_BAT_URL; + const baseurlpost =`${baseurl}/booking`; + const jsonpostheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + const payload = JSON.stringify({ + firstname: `smoke_${Date.now()}`, + lastname: 'Brown', + totalprice: 111, + depositpaid: true, + bookingdates: { checkin: '2018-01-01', checkout: '2019-01-01' }, + additionalneeds: 'Breakfast', + }); + + const createbooking = http.post(baseurlpost, payload, { headers: jsonpostheaders }); + const bookingidnew = createbooking.json('bookingid'); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + // PATCH - Actualiza el nombre del booking creado + const baseurlpatch =`${baseurl}/booking/${bookingidnew}`; + const tokenpatch = data.token; // Obtén el token del objeto data pasado desde setup() + const jsonpatchheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenpatch}`, // Usa el token obtenido en setup() + }; + const payloadpatch = JSON.stringify({ + firstname: `smoke_${Date.now()}`, // Actualiza el nombre con un valor único + lastname: 'Fernandez', // Actualiza el apellido + }); + + const updatebooking = http.patch(baseurlpatch, payloadpatch, { headers: jsonpatchheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //PUT - Actualiza todo el booking creado + const baseurlput =`${baseurl}/booking/${bookingidnew}`; + const tokenput = data.token; // Obtén el token del objeto data pasado desde setup() + const payloadput = JSON.stringify({ + "firstname" : "James", + "lastname" : "Smith", + "totalprice" : 111, + "depositpaid" : true, + "bookingdates" : { + "checkin" : "2018-01-01", + "checkout" : "2019-01-01" + }, + "additionalneeds" : "Breakfast" + }); + const jsonputheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenput}`, // Usa el token obtenido en setup() + }; + + const putbooking = http.put(baseurlput, payloadput, { headers: jsonputheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //DELETE - Elimina el booking creado + const urldelete = `${baseurl}/booking/${bookingidnew}`; + const tokendelete = data.token; // Obtén el token del objeto data pasado desde setup() + const jsondeleteheaders = { + 'Accept': 'application/json', + 'Cookie': `token=${tokendelete}`, // Usa el token obtenido en setup() + }; + + const deletebooking = http.del(urldelete, null, { headers: jsondeleteheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //GET - Ping - HealthCheck + const urlgetping = `${baseurl}/ping`; + const getping = http.get(urlgetping); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + 'summary.json': JSON.stringify(data, null, 2), + 'summary.html': htmlReport(data), + }; +} + +export function teardown() { + console.log('Teardown: prueba finalizada.'); +} \ No newline at end of file diff --git a/nonfunctional/performance/api_k6/spike_test.js b/nonfunctional/performance/api_k6/spike_test.js new file mode 100644 index 0000000..40009d6 --- /dev/null +++ b/nonfunctional/performance/api_k6/spike_test.js @@ -0,0 +1,157 @@ +// init context: importing modules +import http from 'k6/http'; +import { sleep } from 'k6'; //sleep lo usamos solo si quieres simular respiritos entre pasos +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; +import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'; + + +// init context: define k6 options +export const options = { + scenarios: { + stress_e2e_booking: { + executor: 'ramping-arrival-rate', + startRate: 1, + timeUnit: '1s', + preAllocatedVUs: 10, + maxVUs: 50, + stages: [ + { duration: '1m', target: 5 }, // warmup + { duration: '3m', target: 10 }, + { duration: '5m', target: 15 }, + { duration: '8m', target: 20 }, + { duration: '1m', target: 25 }, + ], + exec: 'default', + tags: { test_type: 'stress', flow: 'e2e_booking' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], // toleras algo de error + http_req_failed: ['rate<0.05'], // hasta 5% errores + http_req_duration: ['p(95)<2000'], // p95 puede subir + }, +}; + +// 1. init code - La idea: en setup() obtienes el token una sola vez y devuelves todo lo que vas a reutilizar en default(). +export function setup() { + // A) Variables de entorno + const baseurl = __ENV.API_BOOKER_BAT_URL; + const username = __ENV.API_BOOKER_BAT_USERNAME; + const password = __ENV.API_BOOKER_BAT_PASSWORD; + + if (!baseurl || !username || !password) { + throw new Error('Faltan variables de entorno: API_BOOKER_BAT_URL, API_BOOKER_BAT_USERNAME, API_BOOKER_BAT_PASSWORD'); + } + + // B) Headers JSON comunes + const jsonheaders = { + 'Content-Type': 'application/json', + }; + + // C) Login y obtención del token + const createtoken = http.post(`${baseurl}/auth`, // Llama al endpoint con verbo POST y crea una URL de manera dinámica + JSON.stringify({ username: username, password: password }), // Cuerpo de la petición, convierte tu objeto JS a texto plano JSON (el formato que espera la API) + { headers: jsonheaders } // Envia encabezados (headers), aquí el Content-Type + ); + const token = createtoken.json('token'); // Extrae el token de la respuesta JSON + + return { + token, + baseurl + }; + +} +// 2. setup code +export default function (data) { + + //Post - Crea un booking + const baseurl = __ENV.API_BOOKER_BAT_URL; + const baseurlpost =`${baseurl}/booking`; + const jsonpostheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + const payload = JSON.stringify({ + firstname: `smoke_${Date.now()}`, + lastname: 'Brown', + totalprice: 111, + depositpaid: true, + bookingdates: { checkin: '2018-01-01', checkout: '2019-01-01' }, + additionalneeds: 'Breakfast', + }); + + const createbooking = http.post(baseurlpost, payload, { headers: jsonpostheaders }); + const bookingidnew = createbooking.json('bookingid'); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + // PATCH - Actualiza el nombre del booking creado + const baseurlpatch =`${baseurl}/booking/${bookingidnew}`; + const tokenpatch = data.token; // Obtén el token del objeto data pasado desde setup() + const jsonpatchheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenpatch}`, // Usa el token obtenido en setup() + }; + const payloadpatch = JSON.stringify({ + firstname: `smoke_${Date.now()}`, // Actualiza el nombre con un valor único + lastname: 'Fernandez', // Actualiza el apellido + }); + + const updatebooking = http.patch(baseurlpatch, payloadpatch, { headers: jsonpatchheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //PUT - Actualiza todo el booking creado + const baseurlput =`${baseurl}/booking/${bookingidnew}`; + const tokenput = data.token; // Obtén el token del objeto data pasado desde setup() + const payloadput = JSON.stringify({ + "firstname" : "James", + "lastname" : "Smith", + "totalprice" : 111, + "depositpaid" : true, + "bookingdates" : { + "checkin" : "2018-01-01", + "checkout" : "2019-01-01" + }, + "additionalneeds" : "Breakfast" + }); + const jsonputheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenput}`, // Usa el token obtenido en setup() + }; + + const putbooking = http.put(baseurlput, payloadput, { headers: jsonputheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //DELETE - Elimina el booking creado + const urldelete = `${baseurl}/booking/${bookingidnew}`; + const tokendelete = data.token; // Obtén el token del objeto data pasado desde setup() + const jsondeleteheaders = { + 'Accept': 'application/json', + 'Cookie': `token=${tokendelete}`, // Usa el token obtenido en setup() + }; + + const deletebooking = http.del(urldelete, null, { headers: jsondeleteheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //GET - Ping - HealthCheck + const urlgetping = `${baseurl}/ping`; + const getping = http.get(urlgetping); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + 'summary.json': JSON.stringify(data, null, 2), + 'summary.html': htmlReport(data), + }; +} + +export function teardown() { + console.log('Teardown: prueba finalizada.'); +} \ No newline at end of file diff --git a/nonfunctional/performance/api_k6/stress_test.js b/nonfunctional/performance/api_k6/stress_test.js new file mode 100644 index 0000000..700c0d7 --- /dev/null +++ b/nonfunctional/performance/api_k6/stress_test.js @@ -0,0 +1,160 @@ +// init context: importing modules +import http from 'k6/http'; +import { sleep } from 'k6'; //sleep lo usamos solo si quieres simular respiritos entre pasos +import { check } from 'k6'; //check te permite afirmar “status 200”, “tiene id”, etc. +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; +import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'; + + +// init context: define k6 options +export const options = { + scenarios: { + stress_e2e_booking: { + executor: 'ramping-arrival-rate', + startRate: 1, + timeUnit: '1s', + preAllocatedVUs: 10, + maxVUs: 50, + stages: [ + { duration: '2m', target: 5 }, // warmup + { duration: '3m', target: 10 }, + { duration: '3m', target: 15 }, + { duration: '3m', target: 20 }, + { duration: '3m', target: 25 }, + { duration: '3m', target: 30 }, + { duration: '2m', target: 0 }, // cooldown + ], + exec: 'default', + tags: { test_type: 'stress', flow: 'e2e_booking' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], // toleras algo de error + http_req_failed: ['rate<0.05'], // hasta 5% errores + http_req_duration: ['p(95)<2000'], // p95 puede subir + }, +}; + +// 1. init code - La idea: en setup() obtienes el token una sola vez y devuelves todo lo que vas a reutilizar en default(). +export function setup() { + // A) Variables de entorno + const baseurl = __ENV.API_BOOKER_BAT_URL; + const username = __ENV.API_BOOKER_BAT_USERNAME; + const password = __ENV.API_BOOKER_BAT_PASSWORD; + + if (!baseurl || !username || !password) { + throw new Error('Faltan variables de entorno: API_BOOKER_BAT_URL, API_BOOKER_BAT_USERNAME, API_BOOKER_BAT_PASSWORD'); + } + + // B) Headers JSON comunes + const jsonheaders = { + 'Content-Type': 'application/json', + }; + + // C) Login y obtención del token + const createtoken = http.post(`${baseurl}/auth`, // Llama al endpoint con verbo POST y crea una URL de manera dinámica + JSON.stringify({ username: username, password: password }), // Cuerpo de la petición, convierte tu objeto JS a texto plano JSON (el formato que espera la API) + { headers: jsonheaders } // Envia encabezados (headers), aquí el Content-Type + ); + const token = createtoken.json('token'); // Extrae el token de la respuesta JSON + + return { + token, + baseurl + }; + +} +// 2. setup code +export default function (data) { + + //Post - Crea un booking + const baseurl = __ENV.API_BOOKER_BAT_URL; + const baseurlpost =`${baseurl}/booking`; + const jsonpostheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + const payload = JSON.stringify({ + firstname: `smoke_${Date.now()}`, + lastname: 'Brown', + totalprice: 111, + depositpaid: true, + bookingdates: { checkin: '2018-01-01', checkout: '2019-01-01' }, + additionalneeds: 'Breakfast', + }); + + const createbooking = http.post(baseurlpost, payload, { headers: jsonpostheaders }); + const bookingidnew = createbooking.json('bookingid'); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + // PATCH - Actualiza el nombre del booking creado + const baseurlpatch =`${baseurl}/booking/${bookingidnew}`; + const tokenpatch = data.token; // Obtén el token del objeto data pasado desde setup() + const jsonpatchheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenpatch}`, // Usa el token obtenido en setup() + }; + const payloadpatch = JSON.stringify({ + firstname: `smoke_${Date.now()}`, // Actualiza el nombre con un valor único + lastname: 'Fernandez', // Actualiza el apellido + }); + + const updatebooking = http.patch(baseurlpatch, payloadpatch, { headers: jsonpatchheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //PUT - Actualiza todo el booking creado + const baseurlput =`${baseurl}/booking/${bookingidnew}`; + const tokenput = data.token; // Obtén el token del objeto data pasado desde setup() + const payloadput = JSON.stringify({ + "firstname" : "James", + "lastname" : "Smith", + "totalprice" : 111, + "depositpaid" : true, + "bookingdates" : { + "checkin" : "2018-01-01", + "checkout" : "2019-01-01" + }, + "additionalneeds" : "Breakfast" + }); + const jsonputheaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': `token=${tokenput}`, // Usa el token obtenido en setup() + }; + + const putbooking = http.put(baseurlput, payloadput, { headers: jsonputheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //DELETE - Elimina el booking creado + const urldelete = `${baseurl}/booking/${bookingidnew}`; + const tokendelete = data.token; // Obtén el token del objeto data pasado desde setup() + const jsondeleteheaders = { + 'Accept': 'application/json', + 'Cookie': `token=${tokendelete}`, // Usa el token obtenido en setup() + }; + + const deletebooking = http.del(urldelete, null, { headers: jsondeleteheaders }); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) + + + //GET - Ping - HealthCheck + const urlgetping = `${baseurl}/ping`; + const getping = http.get(urlgetping); + sleep(1); // Simula un tiempo de espera entre acciones (opcional) +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + 'summary.json': JSON.stringify(data, null, 2), + 'summary.html': htmlReport(data), + }; +} + +export function teardown() { + console.log('Teardown: prueba finalizada.'); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fefa4f6..d705696 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@types/date-fns": "^2.6.3", "@types/node": "^24.5.1", "dotenv": "^17.2.2", + "k6": "^0.0.0", "prettier": "^3.6.2", "typescript": "^5.9.2" }, @@ -92,6 +93,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/k6": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/k6/-/k6-0.0.0.tgz", + "integrity": "sha512-GAQSWayS2+LjbH5bkRi+pMPYyP1JSp7o+4j58ANZ762N/RH/SdlAT3CHHztnn8s/xgg8kYNM24Gd2IPo9b5W+g==", + "dev": true + }, "node_modules/playwright": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", diff --git a/package.json b/package.json index 6262414..7c7a705 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,34 @@ "debug": "playwright test --debug", "format": "prettier . --write", "test:strict": "playwright test --forbid-only", - "pretest": "prettier . --check" - }, + "pretest": "prettier . --check", + "test:api": "playwright test --config=api.config.ts", + "test:chrome": "playwright test --config=playwright.config.ts --project=chromium", + "test:firefox": "playwright test --config=playwright.config.ts --project=firefox", + "test:webkit": "playwright test --config=playwright.config.ts --project=webkit", + "test:chromemobile": "playwright test --config=playwright.config.ts --project=Mobile-Chrome", + "test:safarimobile": "playwright test --config=playwright.config.ts --project-Mobile-Safari", + "test:visual:chrome": "playwright test --config=visual.config.ts --project=chromium", + "test:visual:firefox": "playwright test --confi=-visual.config.ts --project=firefox", + "test:visual:webkit": "playwright test --config=visual.config.ts --project=webkit", + "test:visual:chromemobile": "playwright test --config=visual.config.ts --project=Mobile-Chrome", + "test:visual:safarimobile": "playwright test --config=visual.config.ts --project-Mobile-Safari", + "test:api:chrome": "playwright test --config=api.config.ts --project=chromium", + "test:api:firefox": "playwright test --config=api.config.ts --project=firefox", + "test:api:webkit": "playwright test --config=api.config.ts --project=webkit", + "test:api:chromemobile": "playwright test --config=api.config.ts --project=Mobile-Chrome", + "test:api:safarimobile": "playwright test --config=api.config.ts --project-Mobile-Safari", + "k6:smoke": "\"C:\\ProgramData\\chocolatey\\bin\\k6.exe\" run -e API_BOOKER_BAT_URL=https://restful-booker.herokuapp.com -e API_BOOKER_BAT_USERNAME=admin -e API_BOOKER_BAT_PASSWORD=password123 ./nonfunctional/performance/api_k6/smoke_test.js", + "k6:load": "\"C:\\ProgramData\\chocolatey\\bin\\k6.exe\" run -e API_BOOKER_BAT_URL=https://restful-booker.herokuapp.com -e API_BOOKER_BAT_USERNAME=admin -e API_BOOKER_BAT_PASSWORD=password123 ./nonfunctional/performance/api_k6/load_test.js", + "k6:stress": "\"C:\\ProgramData\\chocolatey\\bin\\k6.exe\" run -e API_BOOKER_BAT_URL=https://restful-booker.herokuapp.com -e API_BOOKER_BAT_USERNAME=admin -e API_BOOKER_BAT_PASSWORD=password123 ./nonfunctional/performance/api_k6/stress_test.js", + "k6:spike": "\"C:\\ProgramData\\chocolatey\\bin\\k6.exe\" run -e API_BOOKER_BAT_URL=https://restful-booker.herokuapp.com -e API_BOOKER_BAT_USERNAME=admin -e API_BOOKER_BAT_PASSWORD=password123 ./nonfunctional/performance/api_k6/spike_test.js", + "k6:soak": "\"C:\\ProgramData\\chocolatey\\bin\\k6.exe\" run -e API_BOOKER_BAT_URL=https://restful-booker.herokuapp.com -e API_BOOKER_BAT_USERNAME=admin -e API_BOOKER_BAT_PASSWORD=password123 ./nonfunctional/performance/api_k6/soak_test.js" + + + + +}, + "repository": { "type": "git", "url": "git+https://github.com/javierjimenezdp/qa-automation-playwright.git" @@ -33,11 +59,13 @@ "dependencies": { "date-fns": "^4.1.0" }, + "devDependencies": { "@playwright/test": "^1.55.0", "@types/date-fns": "^2.6.3", "@types/node": "^24.5.1", "dotenv": "^17.2.2", + "k6": "^0.0.0", "prettier": "^3.6.2", "typescript": "^5.9.2" }, diff --git a/playwright.config.ts b/playwright.config.ts index 601134d..6ac2df9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,9 @@ import { defineConfig, devices } from "@playwright/test"; import "dotenv/config"; - const isCI = !!process.env.CI; export default defineConfig({ - testDir: "./test", + testDir: "./src/test/e2e", fullyParallel: true, forbidOnly: isCI, retries: isCI ? 2 : 0, @@ -25,6 +24,7 @@ export default defineConfig({ trace: "on-first-retry", video: "retain-on-failure", screenshot: "only-on-failure", + serviceWorkers: "block", }, projects: [ diff --git a/pre-toggle.png b/pre-toggle.png new file mode 100644 index 0000000..ec24237 Binary files /dev/null and b/pre-toggle.png differ diff --git a/src/pages/01.homepage.ts b/src/pages/01.homepage.ts new file mode 100644 index 0000000..5413eac --- /dev/null +++ b/src/pages/01.homepage.ts @@ -0,0 +1,63 @@ +import { expect, Locator, Page } from "@playwright/test"; + +export class Homepage { + readonly page: Page; + readonly container: Locator; + readonly cardbody: Locator; + readonly rooms: Locator; + readonly location: Locator; + readonly contact: Locator; + + constructor(page: Page) { + this.page = page; + this.container = page + .locator("section") + .filter({ hasText: "Welcome to Shady Meadows B&" }); + this.cardbody = page.locator("#booking"); + this.rooms = page.locator("#rooms"); + this.location = page.locator("#location"); + this.contact = page.locator("#contact"); + } + async visit() { + await this.page.goto("/"); + } + + async containersnapshot() { + await expect(this.container).toBeVisible(); + await expect(await this.container.screenshot()).toMatchSnapshot( + "homepage.png", + { maxDiffPixelRatio: 0.02 }, + ); + } + + async cardbodysnapshot() { + await expect(this.cardbody).toBeVisible(); + await expect(await this.cardbody.screenshot()).toMatchSnapshot( + "cardbody.png", + { maxDiffPixelRatio: 0.02 }, + ); + } + + async roomssnapshot() { + await expect(this.rooms).toBeVisible(); + await expect(await this.rooms.screenshot()).toMatchSnapshot("rooms.png", { + maxDiffPixelRatio: 0.02, + }); + } + + async locationsnapshot() { + await expect(this.location).toBeVisible(); + await expect(await this.location.screenshot()).toMatchSnapshot( + "location.png", + { maxDiffPixelRatio: 0.02 }, + ); + } + + async contactsnapshot() { + await expect(this.contact).toBeVisible(); + await expect(await this.contact.screenshot()).toMatchSnapshot( + "contact.png", + { maxDiffPixelRatio: 0.02 }, + ); + } +} diff --git a/src/pages/02.booknow.ts b/src/pages/02.booknow.ts new file mode 100644 index 0000000..164a714 --- /dev/null +++ b/src/pages/02.booknow.ts @@ -0,0 +1,370 @@ +import { expect, Locator, Page } from "@playwright/test"; + +export class Booknow { + readonly page: Page; + readonly bottonnow: Locator; + + readonly ourrooms: Locator; + readonly availability: Locator; + readonly firstoption: Locator; + readonly photo: Locator; + readonly description: Locator; + readonly tvservice: Locator; + readonly wifiservice: Locator; + readonly safeservice: Locator; + readonly costservice: Locator; + + readonly bottonbooknow: Locator; + readonly photo2slide: Locator; + readonly description2slide: Locator; + readonly tvservice2slide: Locator; + readonly wifiservice2slide: Locator; + readonly safeservice2slide: Locator; + readonly costservice2slide: Locator; + readonly similarrooms2slide: Locator; + readonly selectdate: Locator; + readonly reservenow: Locator; + readonly formreserve: Locator; + readonly firstnameinput: Locator; + readonly lastnameinput: Locator; + readonly emailinput: Locator; + readonly phoneinput: Locator; + readonly reservenowinput: Locator; + readonly cancelinput: Locator; + readonly successmessage: Locator; + readonly errormessagegeneral: Locator; + readonly errrormessagespecific: Locator; + + constructor(page: Page) { + this.page = page; + this.bottonnow = page + .locator("#root-container .hero.py-5 .py-5 .row.py-5 .btn-lg") + .nth(0); + + this.ourrooms = page.locator("#rooms"); + this.availability = page.locator("#rooms div"); + + this.firstoption = page + .locator("#rooms .container .row.g-4 .col-lg-4") + .first(); + this.photo = page + .locator("#rooms .row.g-4 .col-md-6 .room-image .card-img-top") + .first(); + this.description = page + .locator("#rooms .row.g-4 .col-lg-4 .room-card .card-body .card-text") + .first(); + this.tvservice = page + .locator( + "#rooms .row.g-4 .col-md-6.col-lg-4 .card-body .card-text .text-dark", + ) + .nth(0); + this.wifiservice = page + .locator( + "#rooms .row.g-4 .col-md-6.col-lg-4 .card-body .card-text .text-dark", + ) + .nth(1); + this.safeservice = page + .locator( + "#rooms .row.g-4 .col-md-6.col-lg-4 .card-body .card-text .text-dark", + ) + .nth(2); + this.costservice = page + .locator("#rooms .row.g-4 .col-md-6.col-lg-4 .card-footer .fs-5") + .first(); + + this.bottonbooknow = page.locator("a.btn.btn-primary").nth(1); + + this.photo2slide = page.locator( + "#root-container .my-5 .mb-lg-0 .mb-4 .g-2 .col-12 .hero-image", + ); + this.description2slide = page.locator( + "#root-container .my-5 .mb-lg-0 .mb-4 p", + ); + this.tvservice2slide = page + .locator("#root-container .my-5 .mb-lg-0 .mb-4 .flex-wrap .col-md-4 span") + .nth(0); + this.wifiservice2slide = page + .locator("#root-container .my-5 .mb-lg-0 .mb-4 .flex-wrap .col-md-4 span") + .nth(1); + this.safeservice2slide = page + .locator("#root-container .my-5 .mb-lg-0 .mb-4 .flex-wrap .col-md-4 span") + .nth(2); + this.costservice2slide = page.locator( + "#root-container .my-5 .row .col-lg-4 .mb-4 .me-2", + ); + this.similarrooms2slide = page.locator("#root-container .bg-light.py-5"); + + this.selectdate = page.locator( + "#root-container .my-5 .booking-card .card-body form .rbc-calendar .rbc-month-view", + ); + this.reservenow = page.locator("form #doReservation"); + this.formreserve = page.locator("form"); + this.firstnameinput = page.locator( + 'form .room-booking-form [name="firstname"]', + ); + this.lastnameinput = page.locator('form .mb-3 [name="lastname"]'); + this.emailinput = page.locator('form .mb-3 [name="email"]'); + this.phoneinput = page.locator('form .mb-3 [name="phone"]'); + this.reservenowinput = page + .locator( + "#root-container .my-5 .row .col-lg-4 .booking-card .card-body form .w-100.mb-3", + ) + .nth(0); + this.cancelinput = page + .locator( + "#root-container .my-5 .row .col-lg-4 .booking-card .card-body form .w-100.mb-3", + ) + .nth(1); + this.successmessage = page.locator("body div").nth(1); + this.errormessagegeneral = page.locator( + "#root-container .my-5 .row .col-lg-4 form .alert-danger", + ); + this.errrormessagespecific = page.locator( + ".row .col-lg-4 .card-body form .alert-danger", + ); + } + + async gotorroms() { + await this.bottonnow.click(); + await this.ourrooms.scrollIntoViewIfNeeded(); + await expect(this.ourrooms).toBeVisible(); + await expect(this.ourrooms).toHaveText(/Our Rooms/); + } + + async viewandgetservicesfirstslide() { + await expect(this.firstoption).toBeVisible(); + await expect(this.photo).toHaveAttribute("src", { timeout: 10000 }); + await expect(this.photo).toBeVisible(); + const srcphoto = await this.photo.getAttribute("src"); + expect(srcphoto).toBeTruthy(); + console.log("Photo source is: " + srcphoto); + + await expect(this.description).toBeVisible(); + const textdescription = await this.description.textContent(); + expect(textdescription).toBeTruthy(); + console.log("Description text is: " + textdescription); + + await expect(this.tvservice).toBeVisible(); + const texttv = await this.tvservice.textContent(); + expect(texttv).toBeTruthy(); + console.log("TV service text is: " + texttv); + const clean_tv = (texttv ?? "").trim().toLowerCase(); + + await expect(this.wifiservice).toBeVisible(); + const textwifi = await this.wifiservice.textContent(); + expect(textwifi).toBeTruthy(); + console.log("WiFi service text is: " + textwifi); + const clean_wifi = (textwifi ?? "").trim().toLowerCase(); + + await expect(this.safeservice).toBeVisible(); + const textsafe = await this.safeservice.textContent(); + expect(textsafe).toBeTruthy(); + console.log("Safe service text is: " + textsafe); + const clean_safe = (textsafe ?? "").trim().toLowerCase(); + + const textcost = await this.costservice.textContent(); + expect(textcost).toBeTruthy(); + console.log("Cost service text is: " + textcost); + const clean = (textcost ?? "").trim(); + const digits = clean.replace(/[^\d]/g, ""); + const price = Number(digits); + + return { + src: srcphoto, + description: textdescription, + tv: clean_tv, + wifi: clean_wifi, + safe: clean_safe, + cost: price, + }; + } + + async clickfirstroom() { + await this.bottonbooknow.click(); + } + + async viewandgetservicessecondslide() { + await this.photo2slide.scrollIntoViewIfNeeded(); + await expect(this.photo2slide).toBeVisible(); + await expect(this.photo2slide).toHaveAttribute("src", { timeout: 10000 }); + const srcphoto2 = await this.photo2slide.getAttribute("src"); + expect(srcphoto2).toBeTruthy(); + console.log("Photo source 2nd slide is: " + srcphoto2); + + await expect(this.description2slide).toBeVisible(); + const textdescription2 = await this.description2slide.textContent(); + expect(textdescription2).toBeTruthy(); + console.log("Description text 2nd slide is: " + textdescription2); + + await expect(this.tvservice2slide).toBeVisible(); + const texttv2 = await this.tvservice2slide.textContent(); + expect(texttv2).toBeTruthy(); + console.log("TV service text 2nd slide is: " + texttv2); + const clean_tv2 = (texttv2 ?? "").trim().toLowerCase(); + + await expect(this.wifiservice2slide).toBeVisible(); + const textwifi2 = await this.wifiservice2slide.textContent(); + expect(textwifi2).toBeTruthy(); + console.log("WiFi service text 2nd slide is: " + textwifi2); + const clean_wifi2 = (textwifi2 ?? "").trim().toLowerCase(); + + await expect(this.safeservice2slide).toBeVisible(); + const textsafe2 = await this.safeservice2slide.textContent(); + expect(textsafe2).toBeTruthy(); + console.log("Safe service text 2nd slide is: " + textsafe2); + const clean_safe2 = (textsafe2 ?? "").trim().toLowerCase(); + + const textcost2 = await this.costservice2slide.textContent(); + expect(textcost2).toBeTruthy(); + console.log("Cost service text 2nd slide is: " + textcost2); + const clean2 = (textcost2 ?? "").trim(); + const digits2 = clean2.replace(/[^\d]/g, ""); + const price2 = Number(digits2); + + await expect(this.similarrooms2slide).toBeVisible(); + + return { + src: srcphoto2, + description: textdescription2, + tv: clean_tv2, + wifi: clean_wifi2, + safe: clean_safe2, + cost: price2, + }; + } + + async reservearoomcancel( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .col-lg-4 form .mb-4 .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.cancelinput.click(); + await expect(this.selectdate).toBeVisible(); + } + + async reserveroomerrorspecific( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .col-lg-4 form .mb-4 .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.reservenowinput.click(); + await this.errrormessagespecific.scrollIntoViewIfNeeded(); + await expect(this.errrormessagespecific).toBeVisible(); + const errortext = await this.errrormessagespecific.textContent(); + expect(errortext).toBeTruthy(); + console.log("Error message text is: " + errortext); + } + + async reserveroomerrorgeneral( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .col-lg-4 form .mb-4 .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.reservenowinput.click(); + await this.errormessagegeneral.scrollIntoViewIfNeeded(); + await expect(this.errormessagegeneral).toBeVisible(); + const errortext = await this.errormessagegeneral.textContent(); + expect(errortext).toBeTruthy(); + console.log("Error message text is: " + errortext); + } + + async reservearoomsuccess( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .booking-card .card-body form .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.reservenowinput.click(); + await expect(this.successmessage).toBeVisible(); + const successmsg = await this.successmessage.textContent(); + expect(successmsg).toBeTruthy(); + console.log("Success message text is: " + successmsg); + } +} diff --git a/src/pages/03.booknownavbar.ts b/src/pages/03.booknownavbar.ts new file mode 100644 index 0000000..3624029 --- /dev/null +++ b/src/pages/03.booknownavbar.ts @@ -0,0 +1,361 @@ +import { expect, Locator, Page } from "@playwright/test"; + +export class Booknownav { + readonly page: Page; + + readonly ourrooms: Locator; + readonly availability: Locator; + readonly firstoption: Locator; + readonly photo: Locator; + readonly description: Locator; + readonly tvservice: Locator; + readonly wifiservice: Locator; + readonly safeservice: Locator; + readonly costservice: Locator; + + readonly bottonbooknow: Locator; + readonly photo2slide: Locator; + readonly description2slide: Locator; + readonly tvservice2slide: Locator; + readonly wifiservice2slide: Locator; + readonly safeservice2slide: Locator; + readonly costservice2slide: Locator; + readonly similarrooms2slide: Locator; + readonly selectdate: Locator; + readonly reservenow: Locator; + readonly formreserve: Locator; + readonly firstnameinput: Locator; + readonly lastnameinput: Locator; + readonly emailinput: Locator; + readonly phoneinput: Locator; + readonly reservenowinput: Locator; + readonly cancelinput: Locator; + readonly successmessage: Locator; + readonly errormessagegeneral: Locator; + readonly errrormessagespecific: Locator; + + constructor(page: Page) { + this.page = page; + + this.ourrooms = page.locator("#rooms"); + this.availability = page.locator("#rooms div"); + + this.firstoption = page + .locator("#rooms .container .row.g-4 .col-lg-4") + .first(); + this.photo = page + .locator("#rooms .row.g-4 .col-md-6 .room-image .card-img-top") + .first(); + this.description = page + .locator("#rooms .row.g-4 .col-lg-4 .room-card .card-body .card-text") + .first(); + this.tvservice = page + .locator( + "#rooms .row.g-4 .col-md-6.col-lg-4 .card-body .card-text .text-dark", + ) + .nth(0); + this.wifiservice = page + .locator( + "#rooms .row.g-4 .col-md-6.col-lg-4 .card-body .card-text .text-dark", + ) + .nth(1); + this.safeservice = page + .locator( + "#rooms .row.g-4 .col-md-6.col-lg-4 .card-body .card-text .text-dark", + ) + .nth(2); + this.costservice = page + .locator("#rooms .row.g-4 .col-md-6.col-lg-4 .card-footer .fs-5") + .first(); + + this.bottonbooknow = page.locator("a.btn.btn-primary").nth(1); + + this.photo2slide = page.locator( + "#root-container .my-5 .mb-lg-0 .mb-4 .g-2 .col-12 .hero-image", + ); + this.description2slide = page.locator( + "#root-container .my-5 .mb-lg-0 .mb-4 p", + ); + this.tvservice2slide = page + .locator("#root-container .my-5 .mb-lg-0 .mb-4 .flex-wrap .col-md-4 span") + .nth(0); + this.wifiservice2slide = page + .locator("#root-container .my-5 .mb-lg-0 .mb-4 .flex-wrap .col-md-4 span") + .nth(1); + this.safeservice2slide = page + .locator("#root-container .my-5 .mb-lg-0 .mb-4 .flex-wrap .col-md-4 span") + .nth(2); + this.costservice2slide = page.locator( + "#root-container .my-5 .row .col-lg-4 .mb-4 .me-2", + ); + this.similarrooms2slide = page.locator("#root-container .bg-light.py-5"); + + this.selectdate = page.locator( + "#root-container .my-5 .booking-card .card-body form .rbc-calendar .rbc-month-view", + ); + this.reservenow = page.locator("form #doReservation"); + this.formreserve = page.locator("form"); + this.firstnameinput = page.locator( + 'form .room-booking-form [name="firstname"]', + ); + this.lastnameinput = page.locator('form .mb-3 [name="lastname"]'); + this.emailinput = page.locator('form .mb-3 [name="email"]'); + this.phoneinput = page.locator('form .mb-3 [name="phone"]'); + this.reservenowinput = page + .locator( + "#root-container .my-5 .row .col-lg-4 .booking-card .card-body form .w-100.mb-3", + ) + .nth(0); + this.cancelinput = page + .locator( + "#root-container .my-5 .row .col-lg-4 .booking-card .card-body form .w-100.mb-3", + ) + .nth(1); + this.successmessage = page.locator("body div").nth(1); + this.errormessagegeneral = page.locator( + "#root-container .my-5 .row .col-lg-4 form .alert-danger", + ); + this.errrormessagespecific = page.locator( + ".row .col-lg-4 .card-body form .alert-danger", + ); + } + + async gotorroms() { + await expect(this.ourrooms).toBeVisible(); + await expect(this.ourrooms).toHaveText(/Our Rooms/); + } + + async viewandgetservicesfirstslide() { + await this.firstoption.scrollIntoViewIfNeeded(); + await expect(this.firstoption).toBeVisible(); + + await expect(this.photo).toBeVisible(); + const srcphoto = await this.photo.getAttribute("src"); + expect(srcphoto).toBeTruthy(); + console.log("Photo source is: " + srcphoto); + + await expect(this.description).toBeVisible(); + const textdescription = await this.description.textContent(); + expect(textdescription).toBeTruthy(); + console.log("Description text is: " + textdescription); + + await expect(this.tvservice).toBeVisible(); + const texttv = await this.tvservice.textContent(); + expect(texttv).toBeTruthy(); + console.log("TV service text is: " + texttv); + const clean_tv = (texttv ?? "").trim().toLowerCase(); + + await expect(this.wifiservice).toBeVisible(); + const textwifi = await this.wifiservice.textContent(); + expect(textwifi).toBeTruthy(); + console.log("WiFi service text is: " + textwifi); + const clean_wifi = (textwifi ?? "").trim().toLowerCase(); + + await expect(this.safeservice).toBeVisible(); + const textsafe = await this.safeservice.textContent(); + expect(textsafe).toBeTruthy(); + console.log("Safe service text is: " + textsafe); + const clean_safe = (textsafe ?? "").trim().toLowerCase(); + + const textcost = await this.costservice.textContent(); + expect(textcost).toBeTruthy(); + console.log("Cost service text is: " + textcost); + const clean = (textcost ?? "").trim(); + const digits = clean.replace(/[^\d]/g, ""); + const price = Number(digits); + + return { + src: srcphoto, + description: textdescription, + tv: clean_tv, + wifi: clean_wifi, + safe: clean_safe, + cost: price, + }; + } + + async clickfirstroom() { + await this.bottonbooknow.click(); + } + + async viewandgetservicessecondslide() { + await this.photo2slide.scrollIntoViewIfNeeded(); + await expect(this.photo2slide).toBeVisible(); + const srcphoto2 = await this.photo2slide.getAttribute("src"); + expect(srcphoto2).toBeTruthy(); + console.log("Photo source 2nd slide is: " + srcphoto2); + + await expect(this.description2slide).toBeVisible(); + const textdescription2 = await this.description2slide.textContent(); + expect(textdescription2).toBeTruthy(); + console.log("Description text 2nd slide is: " + textdescription2); + + await expect(this.tvservice2slide).toBeVisible(); + const texttv2 = await this.tvservice2slide.textContent(); + expect(texttv2).toBeTruthy(); + console.log("TV service text 2nd slide is: " + texttv2); + const clean_tv2 = (texttv2 ?? "").trim().toLowerCase(); + + await expect(this.wifiservice2slide).toBeVisible(); + const textwifi2 = await this.wifiservice2slide.textContent(); + expect(textwifi2).toBeTruthy(); + console.log("WiFi service text 2nd slide is: " + textwifi2); + const clean_wifi2 = (textwifi2 ?? "").trim().toLowerCase(); + + await expect(this.safeservice2slide).toBeVisible(); + const textsafe2 = await this.safeservice2slide.textContent(); + expect(textsafe2).toBeTruthy(); + console.log("Safe service text 2nd slide is: " + textsafe2); + const clean_safe2 = (textsafe2 ?? "").trim().toLowerCase(); + + const textcost2 = await this.costservice2slide.textContent(); + expect(textcost2).toBeTruthy(); + console.log("Cost service text 2nd slide is: " + textcost2); + const clean2 = (textcost2 ?? "").trim(); + const digits2 = clean2.replace(/[^\d]/g, ""); + const price2 = Number(digits2); + + await expect(this.similarrooms2slide).toBeVisible(); + + return { + src: srcphoto2, + description: textdescription2, + tv: clean_tv2, + wifi: clean_wifi2, + safe: clean_safe2, + cost: price2, + }; + } + + async reservearoomcancel( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .col-lg-4 form .mb-4 .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.cancelinput.click(); + await expect(this.selectdate).toBeVisible(); + } + + async reserveroomerrorspecific( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .col-lg-4 form .mb-4 .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.reservenowinput.click(); + await this.errrormessagespecific.scrollIntoViewIfNeeded(); + await expect(this.errrormessagespecific).toBeVisible(); + const errortext = await this.errrormessagespecific.textContent(); + expect(errortext).toBeTruthy(); + console.log("Error message text is: " + errortext); + } + + async reserveroomerrorgeneral( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .col-lg-4 form .mb-4 .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.reservenowinput.click(); + await this.errormessagegeneral.scrollIntoViewIfNeeded(); + await expect(this.errormessagegeneral).toBeVisible(); + const errortext = await this.errormessagegeneral.textContent(); + expect(errortext).toBeTruthy(); + console.log("Error message text is: " + errortext); + } + + async reservearoomsuccess( + firstname: string, + lastname: string, + email: string, + phone: string, + page: Page, + ) { + await this.selectdate.scrollIntoViewIfNeeded(); + await expect(this.selectdate).toBeVisible(); + const calendar = page.locator( + "#root-container .my-5 .booking-card .card-body form .rbc-calendar .rbc-month-view", + ); + const selectevent = calendar.locator( + '.rbc-event-content[title="Selected"]', + ); + await expect(selectevent).toBeVisible(); + await expect(selectevent).toHaveCount(1); + + await this.reservenow.scrollIntoViewIfNeeded(); + await this.reservenow.click(); + + await expect(this.formreserve).toBeVisible(); + await this.firstnameinput.fill(firstname); + await this.lastnameinput.fill(lastname); + await this.emailinput.fill(email); + await this.phoneinput.fill(phone); + + await this.reservenowinput.click(); + await expect(this.successmessage).toBeVisible(); + } +} diff --git a/src/pages/04.booknowdatapicker.ts b/src/pages/04.booknowdatapicker.ts new file mode 100644 index 0000000..b0f22d1 --- /dev/null +++ b/src/pages/04.booknowdatapicker.ts @@ -0,0 +1,100 @@ +import { expect, Locator, Page } from "@playwright/test"; + +export class Booknowdatapicker { + readonly page: Page; + readonly booking: Locator; + readonly selectdatecheckin: Locator; + readonly selectdatecheckout: Locator; + readonly checkavaialibility: Locator; + readonly firstcardimage: Locator; + + constructor(page: Page) { + this.page = page; + this.booking = page.locator("#root-container #booking"); + this.selectdatecheckin = page + .locator( + "#root-container #booking .container form .col-md-6 .form-control", + ) + .nth(0); + this.selectdatecheckout = page + .locator( + "#root-container #booking .container form .col-md-6 .form-control", + ) + .nth(1); + this.checkavaialibility = page.locator( + "#root-container #booking .container form .col-8.mt-4 .btn-primary", + ); + this.firstcardimage = page + .locator("#rooms .container .row.g-4 .col-lg-4 img") + .first(); + } + + async bookingpage() { + await this.booking.scrollIntoViewIfNeeded(); + await expect(this.booking).toBeVisible(); + } + + async booknowdatecheckin(page: Page) { + const inputs = this.selectdatecheckin; + const today = new Date(); + const nextWeek = new Date(today); + nextWeek.setDate(today.getDate() + 7); + + const day = nextWeek.getDate(); + const month = nextWeek.toLocaleDateString("en-US", { month: "long" }); + const year = nextWeek.getFullYear(); + + await inputs.click(); + + const header = this.page.locator(".react-datepicker__current-month"); + for (let i = 0; i < 12; i++) { + const current = (await header.textContent())?.trim(); + if (current === `${month} ${year}`) break; + await this.page.locator(".react-datepicker__navigation--next").click(); + } + + const daySelector = `.react-datepicker .react-datepicker__day:not(.react-datepicker__day--outside-month).react-datepicker__day--${String(day).padStart(3, "0")}`; + await this.page.waitForSelector(".react-datepicker__month"); + await this.page.locator(daySelector).click(); + } + + async booknowdatecheckout(page: Page) { + const inputs = this.selectdatecheckout; + const today = new Date(); + const nextWeek = new Date(today); + nextWeek.setDate(today.getDate() + 15); + + const day = nextWeek.getDate(); + const month = nextWeek.toLocaleDateString("en-US", { month: "long" }); + const year = nextWeek.getFullYear(); + + await inputs.click(); + + const header = this.page.locator(".react-datepicker__current-month"); + for (let i = 0; i < 12; i++) { + const current = (await header.textContent())?.trim(); + if (current === `${month} ${year}`) break; + await this.page.locator(".react-datepicker__navigation--next").click(); + } + + const daySelector = `.react-datepicker .react-datepicker__day:not(.react-datepicker__day--outside-month).react-datepicker__day--${String(day).padStart(3, "0")}`; + await this.page.waitForSelector(".react-datepicker__month"); + await this.page.locator(daySelector).click(); + } + + async checkavaialibilityclick() { + await this.checkavaialibility.scrollIntoViewIfNeeded(); + await expect(this.checkavaialibility).toBeVisible(); + await Promise.all([ + this.page.waitForLoadState("networkidle"), + this.checkavaialibility.click(), + ]); + await this.page.waitForTimeout(3500); + } + async waitFirstCardReady() { + await expect(this.firstcardimage).toBeVisible({ timeout: 10_000 }); + await expect(this.firstcardimage).toHaveAttribute("src", /.+/, { + timeout: 10_000, + }); + } +} diff --git a/src/pages/05.location.ts b/src/pages/05.location.ts new file mode 100644 index 0000000..d1d5e9a --- /dev/null +++ b/src/pages/05.location.ts @@ -0,0 +1,93 @@ +import { expect, Locator, Page } from "@playwright/test"; +import { normalizeText } from "../utils/textutils"; + +export class Location { + readonly page: Page; + readonly locationsection: Locator; + readonly maplocation: Locator; + readonly contactinformationcard: Locator; + readonly contactaddress: Locator; + readonly contactphone: Locator; + readonly contactemail: Locator; + readonly contactgethere: Locator; + + constructor(page: Page) { + this.page = page; + this.locationsection = page.locator("#root-container #location"); + this.maplocation = page + .locator("#root-container #location .container .row.g-4 .col-lg-6") + .filter({ has: page.locator(".pigeon-overlays") }) + .first(); + this.contactinformationcard = page + .locator( + "#root-container #location .container .col-lg-6 .h-100 .card-body", + ) + .nth(1); + this.contactaddress = page + .locator( + "#root-container #location .container .col-lg-6 .h-100 .card-body .mb-0", + ) + .nth(0); + this.contactphone = page + .locator( + "#root-container #location .container .col-lg-6 .h-100 .card-body .mb-0", + ) + .nth(1); + this.contactemail = page + .locator( + "#root-container #location .container .col-lg-6 .h-100 .card-body .mb-0", + ) + .nth(2); + this.contactgethere = page + .locator( + "#root-container #location .container .col-lg-6 .h-100 .card-body p", + ) + .nth(3); + } + + async locationpagesection() { + await expect(this.locationsection).toBeVisible(); + const screenlocation = await this.locationsection.screenshot(); + await expect(screenlocation).toMatchSnapshot("locationsection.png"); + + await expect(this.maplocation).toBeVisible(); + const screenmap = await this.locationsection.screenshot(); + await expect(screenmap).toMatchSnapshot("maplocation.png"); + + await expect(this.contactinformationcard).toBeVisible(); + const screencontactinfo = await this.locationsection.screenshot(); + await expect(screencontactinfo).toMatchSnapshot( + "contactinformationcard.png", + ); + } + + async contactinformationtext() { + await expect(this.contactaddress).toBeVisible(); + const addres = await this.contactaddress.textContent(); + const addresutils = normalizeText(addres); + console.log("Address:", addresutils); + expect(addresutils).toContain( + "Shady Meadows B&B, Shadows valley, Newingtonfordburyshire, Dilbery, N1 1AA", + ); + + await expect(this.contactphone).toBeVisible(); + const phone = await this.contactphone.textContent(); + const phoneutils = normalizeText(phone); + console.log("Phone:", phoneutils); + expect(phoneutils).toContain("012345678901"); + + await expect(this.contactemail).toBeVisible(); + const email = await this.contactemail.textContent(); + const emailutils = normalizeText(email); + console.log("Email:", emailutils); + expect(emailutils).toContain("fake@fakeemail.com"); + + await expect(this.contactgethere).toBeVisible(); + const gettinghere = await this.contactgethere.textContent(); + const gettinhereutils = normalizeText(gettinghere); + console.log("Getting Here:", gettinhereutils); + expect(gettinhereutils).toContain( + "Welcome to Shady Meadows, a delightful Bed & Breakfast nestled in the hills on Newingtonfordburyshire. A place so beautiful you will never want to leave. All our rooms have comfortable beds and we provide breakfast from the locally sourced supermarket. It is a delightful place.", + ); + } +} diff --git a/src/pages/06.sendmessage.ts b/src/pages/06.sendmessage.ts new file mode 100644 index 0000000..31a1ebf --- /dev/null +++ b/src/pages/06.sendmessage.ts @@ -0,0 +1,153 @@ +import { + expect, + Locator, + LocatorScreenshotOptions, + Page, +} from "@playwright/test"; +import { normalizeText } from "../utils/textutils"; + +export class Sendmessage { + readonly page: Page; + readonly nameinput: Locator; + readonly emailinput: Locator; + readonly phoneinput: Locator; + readonly subjectinput: Locator; + readonly messageinput: Locator; + readonly grongsubmited: Locator; + readonly succesfullysubmited: Locator; + readonly submitbutton: Locator; + + constructor(page: Page) { + this.page = page; + this.nameinput = page + .locator( + "#root-container #contact .container .col-lg-8 .card.shadow .form-control", + ) + .nth(0); + this.emailinput = page + .locator( + "#root-container #contact .container .col-lg-8 .card.shadow .form-control", + ) + .nth(1); + this.phoneinput = page + .locator( + "#root-container #contact .container .col-lg-8 .card.shadow .form-control", + ) + .nth(2); + this.subjectinput = page + .locator( + "#root-container #contact .container .col-lg-8 .card.shadow .form-control", + ) + .nth(3); + this.messageinput = page + .locator( + "#root-container #contact .container .col-lg-8 .card.shadow .form-control", + ) + .nth(4); + this.grongsubmited = page.locator( + "#root-container #contact .container .col-lg-8 .card.shadow .alert.alert-danger", + ); + this.succesfullysubmited = page.locator( + "#root-container #contact .container .col-lg-8 .card.shadow .card-body.p-4", + ); + this.submitbutton = page.locator( + "#root-container #contact .container .col-lg-8 .card.shadow .btn.btn-primary", + ); + } + + async messagesectionclean() { + const nameinputstring = "Javier"; + + await expect(this.nameinput).toBeVisible(); + await this.nameinput.fill(""); + + await expect(this.emailinput).toBeVisible(); + await this.emailinput.fill(""); + + await expect(this.phoneinput).toBeVisible(); + await this.phoneinput.fill(""); + + await expect(this.subjectinput).toBeVisible(); + await this.subjectinput.fill(""); + + await expect(this.messageinput).toBeVisible(); + await this.messageinput.fill(""); + + await this.submitbutton.click(); + await this.grongsubmited.scrollIntoViewIfNeeded(); + await expect(this.grongsubmited).toBeVisible({ timeout: 5000 }); + } + + async messagesectionwrong() { + const nameinputstring = "Javier"; + + await expect(this.nameinput).toBeVisible(); + await this.nameinput.fill(nameinputstring); + + await expect(this.emailinput).toBeVisible(); + await this.emailinput.fill("Testing"); + + await expect(this.phoneinput).toBeVisible(); + await this.phoneinput.fill("javier@testing.com"); + + await expect(this.subjectinput).toBeVisible(); + await this.subjectinput.fill("Test succesfully"); + + await expect(this.messageinput).toBeVisible(); + await this.messageinput.fill( + "Test to each input on the message section to verify it each one of them meet the validations", + ); + + await this.submitbutton.click(); + await this.grongsubmited.scrollIntoViewIfNeeded(); + await expect(this.grongsubmited).toBeVisible({ timeout: 5000 }); + } + + async messagesectionsucesfully() { + const lastnames = [ + "Perez", + "Gomez", + "Lopez", + "Martinez", + "Rodriguez", + "Castro", + "Diaz", + ]; + const randomlastname = + lastnames[Math.floor(Math.random() * lastnames.length)]; + const nameinputstring = "Javier"; + + const randomnumber = Date.now(); + const emailvalue = `javier${randomnumber}@gmail.com`; + + const phoneValue = `3${Math.floor(1000000000 + Math.random() * 9000000000)}`; + const subjectValue = `Subject test with ${randomlastname}`; + const messageValue = `Message test with at least 30 characters using ${randomlastname} validation run.`; + + await expect(this.nameinput).toBeVisible(); + await this.nameinput.fill(`${nameinputstring} ${randomlastname}`); + + await expect(this.emailinput).toBeVisible(); + await this.emailinput.fill(emailvalue); + + await expect(this.phoneinput).toBeVisible(); + await this.phoneinput.fill(phoneValue); + + await expect(this.subjectinput).toBeVisible(); + await this.subjectinput.fill(subjectValue); + + await expect(this.messageinput).toBeVisible(); + await this.messageinput.fill(messageValue); + + await this.submitbutton.click({ timeout: 5000 }); + + await this.succesfullysubmited.scrollIntoViewIfNeeded({ timeout: 5000 }); + await expect(this.succesfullysubmited).toBeVisible({ timeout: 5000 }); + const messagesuccesfully = await this.succesfullysubmited.textContent(); + const messagesuccesfullyutils = normalizeText(messagesuccesfully); + console.log("Message Succesfully:", messagesuccesfullyutils); + expect(messagesuccesfullyutils).toContain( + `${nameinputstring} ${randomlastname}`, + ); + } +} diff --git a/src/pages/components/navbar.ts b/src/pages/components/navbar.ts new file mode 100644 index 0000000..4111152 --- /dev/null +++ b/src/pages/components/navbar.ts @@ -0,0 +1,97 @@ +import { expect, Locator, Page } from "@playwright/test"; + +export class NavBar { + readonly page!: Page; + readonly rooms!: Locator; + readonly booking!: Locator; + readonly amenities!: Locator; + readonly location!: Locator; + readonly contact!: Locator; + readonly admin!: Locator; + readonly collapse!: Locator; + readonly navbartoggler!: Locator; + + constructor(page: Page) { + this.page = page; + this.rooms = page + .locator("#root-container .navbar-light #navbarNav .nav-item .nav-link") + .nth(0); + this.booking = page + .locator("#root-container .navbar-light #navbarNav .nav-item .nav-link") + .nth(1); + this.amenities = page + .locator("#root-container .navbar-light #navbarNav .nav-item .nav-link") + .nth(2); + this.location = page + .locator("#root-container .navbar-light #navbarNav .nav-item .nav-link") + .nth(3); + this.contact = page + .locator("#root-container .navbar-light #navbarNav .nav-item .nav-link") + .nth(4); + this.admin = page + .locator("#root-container .navbar-light #navbarNav .nav-item .nav-link") + .nth(5); + this.collapse = page.locator("#navbarNav"); + this.navbartoggler = page.locator( + '[data-bs-toggle="collapse"][data-bs-target="#navbarNav"], button[aria-controls="navbarNav"]', + ); + } + + async hamburguermenu() { + await this.collapse.waitFor({ state: "attached" }); + await this.page.waitForTimeout(50); + + const count = await this.navbartoggler.count(); + if (count === 0) { + console.log( + "[DEBUG] No encontré el toggler con atributos. HTML del nav:", + ); + console.log(await this.page.locator("nav.navbar").first().innerHTML()); + return; + } + + if (await this.navbartoggler.isVisible()) { + try { + await this.navbartoggler.scrollIntoViewIfNeeded(); + await this.navbartoggler.click(); + } catch { + await this.navbartoggler.evaluate((btn: HTMLElement) => btn.click()); + } + await expect(this.collapse).toBeVisible(); + await this.page.waitForTimeout(150); + } else { + console.log( + "[DEBUG] Toggler no visible (prob. viewport desktop o algo lo tapa).", + ); + } + } + async clickontabtext(tabname: string) { + await this.hamburguermenu(); + switch (tabname) { + case "Rooms": + await expect(this.rooms).toBeVisible(); + await this.rooms.click(); + break; + case "Booking": + await expect(this.booking).toBeVisible(); + await this.booking.click(); + break; + case "Amenities": + await expect(this.amenities).toBeVisible(); + await this.amenities.click(); + break; + case "Location": + await expect(this.location).toBeVisible(); + await this.location.click(); + break; + case "Contact": + await expect(this.contact).toBeVisible(); + await this.contact.click(); + break; + case "Admin": + await expect(this.admin).toBeVisible(); + await this.admin.click(); + break; + } + } +} diff --git a/src/test/API/01.postcreatetoken.spec.ts b/src/test/API/01.postcreatetoken.spec.ts new file mode 100644 index 0000000..51cc7ca --- /dev/null +++ b/src/test/API/01.postcreatetoken.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker TOKEN", () => { + const baseURL = process.env.API_BOOKER_URL; + + test("POST - AuthCreateToken", async ({ request }) => { + const response = await request.post(`${baseURL}/auth`, { + data: { + username: process.env.API_USERNAME, + password: process.env.API_PASSWORD, + }, + }); + const responseBody = JSON.parse(await response.text()); + const token = responseBody.token; + console.log(token); + }); +}); diff --git a/src/test/API/02.getbookingsid.spec.ts b/src/test/API/02.getbookingsid.spec.ts new file mode 100644 index 0000000..283724a --- /dev/null +++ b/src/test/API/02.getbookingsid.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker GET ALL ID", () => { + const baseURL = process.env.API_BOOKER_URL; + + test("GET - GetBookingIds", async ({ request }) => { + const response = await request.get(`${baseURL}/booking`); + const responseBody = JSON.parse(await response.text()); + console.log(responseBody); + }); +}); diff --git a/src/test/API/03.getjustbookingid.spec.ts b/src/test/API/03.getjustbookingid.spec.ts new file mode 100644 index 0000000..b0bc596 --- /dev/null +++ b/src/test/API/03.getjustbookingid.spec.ts @@ -0,0 +1,13 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker GET SPECIFIC ID", () => { + const baseURL = process.env.API_BOOKER_URL; + + test("GET - GetBookingById", async ({ request }) => { + const randomid = `${Math.floor(Math.random() * 20)}`; + console.log(randomid); + const response = await request.get(`${baseURL}/booking/${randomid}`); + const responseBody = JSON.parse(await response.text()); + console.log(responseBody); + }); +}); diff --git a/src/test/API/04.postcreatebooking.spec.ts b/src/test/API/04.postcreatebooking.spec.ts new file mode 100644 index 0000000..37c556b --- /dev/null +++ b/src/test/API/04.postcreatebooking.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker CREATE BOOKING", () => { + const baseURL = process.env.API_BOOKER_URL; + + test("POST - CreateBooking and PUT - UpdateBooking", async ({ request }) => { + //POST - CreateBooking + const response = await request.post(`${baseURL}/booking`, { + data: { + firstname: "Sally", + lastname: "Brown", + totalprice: 111, + depositpaid: true, + bookingdates: { + checkin: "2013-02-23", + checkout: "2014-10-23", + }, + additionalneeds: "Breakfast", + }, + }); + const responseBody = JSON.parse(await response.text()); + const bookingdid = responseBody.bookingid; + console.log(bookingdid); + }); +}); diff --git a/src/test/API/05.putupdatebooking.spec.ts b/src/test/API/05.putupdatebooking.spec.ts new file mode 100644 index 0000000..2670173 --- /dev/null +++ b/src/test/API/05.putupdatebooking.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker UPDATE BOOKING", () => { + const baseURL = process.env.API_BOOKER_URL!; + + test.describe("Create Token and Booking", () => { + let token: string; + let bookingdid: number; + + test.beforeAll(async ({ request }) => { + //1. Get Auth Token + const authResponse = await request.post(`${baseURL}/auth`, { + data: { + username: process.env.API_USERNAME, + password: process.env.API_PASSWORD, + }, + }); + const authBody = JSON.parse(await authResponse.text()); + token = authBody.token; + console.log("TOKEN:", token); + + //2. Create Booking + const createResponse = await request.post(`${baseURL}/booking`, { + data: { + firstname: "Sally", + lastname: "Brown", + totalprice: 111, + depositpaid: true, + bookingdates: { + checkin: "2013-02-23", + checkout: "2014-10-23", + }, + additionalneeds: "Breakfast", + }, + }); + const responseBody = JSON.parse(await createResponse.text()); + console.log(responseBody); + bookingdid = responseBody.bookingid; + console.log("BOOKINGID:", bookingdid); + }); + + test("PUT - UpdateBooking", async ({ request }) => { + const updateResponse = await request.put( + `${baseURL}/booking/${bookingdid}`, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Cookie: `token=${token}`, + }, + data: { + firstname: "SallyUpdated", + lastname: "BrownUpdated", + totalprice: 222, + depositpaid: false, + bookingdates: { + checkin: "2014-02-23", + checkout: "2015-10-23", + }, + additionalneeds: "Lunch", + }, + }, + ); + const updateResponseBody = JSON.parse(await updateResponse.text()); + console.log(updateResponseBody); + }); + }); +}); diff --git a/src/test/API/06.patchpartialupdate.spec.ts b/src/test/API/06.patchpartialupdate.spec.ts new file mode 100644 index 0000000..f281cc3 --- /dev/null +++ b/src/test/API/06.patchpartialupdate.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker UPDATE PARTIAL BOOKING", () => { + const baseURL = process.env.API_BOOKER_URL!; + + test.describe("Create Token and Booking", () => { + let token: string; + let bookingdid: number; + + test.beforeAll(async ({ request }) => { + //1. Get Auth Token + const authResponse = await request.post(`${baseURL}/auth`, { + data: { + username: process.env.API_USERNAME, + password: process.env.API_PASSWORD, + }, + }); + const authBody = JSON.parse(await authResponse.text()); + token = authBody.token; + console.log("TOKEN:", token); + + //2. Create Booking + const createResponse = await request.post(`${baseURL}/booking`, { + data: { + firstname: "Sally", + lastname: "Brown", + totalprice: 111, + depositpaid: true, + bookingdates: { + checkin: "2013-02-23", + checkout: "2014-10-23", + }, + additionalneeds: "Breakfast", + }, + }); + const responseBody = JSON.parse(await createResponse.text()); + console.log(responseBody); + bookingdid = responseBody.bookingid; + console.log("BOOKINGID:", bookingdid); + }); + + test("PATCH - UpdateBooking", async ({ request }) => { + const updateResponse = await request.patch( + `${baseURL}/booking/${bookingdid}`, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Cookie: `token=${token}`, + }, + data: { + firstname: "SallyUpdated", + lastname: "BrownUpdated", + }, + }, + ); + const updateResponseBody = JSON.parse(await updateResponse.text()); + console.log(updateResponseBody); + }); + }); +}); diff --git a/src/test/API/07.deletebookingid.spec.ts b/src/test/API/07.deletebookingid.spec.ts new file mode 100644 index 0000000..13ad78a --- /dev/null +++ b/src/test/API/07.deletebookingid.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker DELETE BOOKING", () => { + const baseURL = process.env.API_BOOKER_URL!; + + test.describe("Create Token and Booking", () => { + let token: string; + let bookingdid: number; + + test.beforeAll(async ({ request }) => { + //1. Get Auth Token + const authResponse = await request.post(`${baseURL}/auth`, { + data: { + username: process.env.API_USERNAME, + password: process.env.API_PASSWORD, + }, + }); + const authBody = JSON.parse(await authResponse.text()); + token = authBody.token; + console.log("TOKEN:", token); + + //2. Create Booking + const createResponse = await request.post(`${baseURL}/booking`, { + data: { + firstname: "Sally", + lastname: "Brown", + totalprice: 111, + depositpaid: true, + bookingdates: { + checkin: "2013-02-23", + checkout: "2014-10-23", + }, + additionalneeds: "Breakfast", + }, + }); + const responseBody = JSON.parse(await createResponse.text()); + console.log(responseBody); + bookingdid = responseBody.bookingid; + console.log("BOOKINGID:", bookingdid); + }); + + test("DELETE - booking", async ({ request }) => { + const deletebooking = await request.delete( + `${baseURL}/booking/${bookingdid}`, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Cookie: `token=${token}`, + }, + }, + ); + await expect(deletebooking.status()).toBe(201); + const text = await deletebooking.text(); + console.log(text); + }); + }); +}); diff --git a/src/test/API/08.gethealthcheck.spec.ts b/src/test/API/08.gethealthcheck.spec.ts new file mode 100644 index 0000000..dad6a43 --- /dev/null +++ b/src/test/API/08.gethealthcheck.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API Booker GET HealthCheck", () => { + const baseURL = process.env.API_BOOKER_URL; + + test("GET - HealthCheck", async ({ request }) => { + const response = await request.get(`${baseURL}/ping`); + expect(response.status()).toBe(201); + }); +}); diff --git a/src/test/e2e/01.homepage.spec.ts b/src/test/e2e/01.homepage.spec.ts new file mode 100644 index 0000000..73bf426 --- /dev/null +++ b/src/test/e2e/01.homepage.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; + +test.describe("Homepage", () => { + let homepage: Homepage; + + test.beforeEach(async ({ page }) => { + homepage = new Homepage(page); + await homepage.visit(); + }); + + test("should display the main container", async () => { + await expect(homepage.container).toBeVisible(); + }); + + test("should display the booking card", async () => { + await expect(homepage.cardbody).toBeVisible(); + }); + + test("should display the rooms section", async () => { + await expect(homepage.rooms).toBeVisible(); + }); + + test("should display the location section", async () => { + await expect(homepage.location).toBeVisible(); + }); + + test("should display the contact section", async () => { + await expect(homepage.contact).toBeVisible(); + }); +}); diff --git a/src/test/e2e/02.booknow.spec.ts b/src/test/e2e/02.booknow.spec.ts new file mode 100644 index 0000000..b7073db --- /dev/null +++ b/src/test/e2e/02.booknow.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; +import { Booknow } from "../../pages/02.booknow"; + +test.describe("Book Now Page", () => { + let booknowpage: Booknow; + let homepage: Homepage; + + test.beforeEach(async ({ page }) => { + booknowpage = new Booknow(page); + homepage = new Homepage(page); + await homepage.visit(); + await booknowpage.gotorroms(); + }); + + test("Save first room values", async ({ page }) => { + const firstValues = await booknowpage.viewandgetservicesfirstslide(); + await booknowpage.clickfirstroom(); + const secondValues = await booknowpage.viewandgetservicessecondslide(); + expect(secondValues.src).toBe(firstValues.src); + expect(secondValues.description).toBe(firstValues.description); + expect(secondValues.tv).toBe(firstValues.tv); + expect(secondValues.wifi).toBe(firstValues.wifi); + expect(secondValues.safe).toBe(firstValues.safe); + expect(secondValues.cost).toBe(firstValues.cost); + }); + + test("Cancel reservation", async ({ page }) => { + await booknowpage.clickfirstroom(); + await booknowpage.reservearoomcancel(" ", " ", " ", " ", page); + }); + + test("Error reservation general", async ({ page }) => { + await booknowpage.clickfirstroom(); + await booknowpage.reserveroomerrorgeneral("", "", "", "", page); + }); + + test("Error reservation specific", async ({ page }) => { + await booknowpage.clickfirstroom(); + await booknowpage.reserveroomerrorspecific( + "Javier", + "Testing", + "javier@testing.com", + "3108948596", + page, + ); + }); + test("Success reservation", async ({ page }) => { + await booknowpage.clickfirstroom(); + await booknowpage.reservearoomsuccess( + "Javier", + "Testing", + "javier@testing.com", + "31089485962", + page, + ); + }); +}); diff --git a/src/test/e2e/03.booknownav.spec.ts b/src/test/e2e/03.booknownav.spec.ts new file mode 100644 index 0000000..b387761 --- /dev/null +++ b/src/test/e2e/03.booknownav.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; +import { Booknownav } from "../../pages/03.booknownavbar"; +import { NavBar } from "../../pages/components/navbar"; + +test.describe("Book Now Page NavBar", () => { + let booknowPage: Booknownav; + let homepage: Homepage; + let navbar: NavBar; + + test.beforeEach(async ({ page }) => { + booknowPage = new Booknownav(page); + homepage = new Homepage(page); + navbar = new NavBar(page); + console.log("toggler count:", await navbar.navbartoggler.count()); + console.log("toggler visible:", await navbar.navbartoggler.isVisible()); + console.log("collapse hidden:", await navbar.collapse.isHidden()); + await page.screenshot({ path: "pre-toggle.png" }); + await homepage.visit(); + }); + + test("Cancel reservation", async ({ page }) => { + await booknowPage.clickfirstroom(); + await booknowPage.reservearoomcancel(" ", " ", " ", " ", page); + }); + + test("Error reservation general", async ({ page }) => { + await booknowPage.clickfirstroom(); + await booknowPage.reserveroomerrorgeneral("", "", "", "", page); + }); + + test("Error reservation specific", async ({ page }) => { + await booknowPage.clickfirstroom(); + await booknowPage.reserveroomerrorspecific( + "Javier", + "Testing", + "javier@testing.com", + "3108948596", + page, + ); + }); + test("Success reservation", async ({ page }) => { + await booknowPage.clickfirstroom(); + await booknowPage.reservearoomsuccess( + "Javier", + "Testing", + "javier@testing.com", + "31089485962", + page, + ); + }); +}); diff --git a/src/test/e2e/04.booknowdatapicker.spec.ts b/src/test/e2e/04.booknowdatapicker.spec.ts new file mode 100644 index 0000000..67362f7 --- /dev/null +++ b/src/test/e2e/04.booknowdatapicker.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; +import { Booknow } from "../../pages/02.booknow"; +import { Booknowdatapicker } from "../../pages/04.booknowdatapicker"; +import { addMonths, addWeeks, addYears, format } from "date-fns"; + +test.describe("Booknow Datepicker Functionality", () => { + let homepage: Homepage; + let booknowpage: Booknow; + let booknowdatapicker: Booknowdatapicker; + + test.beforeEach(async ({ page }) => { + await page.route("**/*", (route) => { + const h = { + ...route.request().headers(), + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", + }; + route.continue({ headers: h }); + }); + homepage = new Homepage(page); + booknowpage = new Booknow(page); + booknowdatapicker = new Booknowdatapicker(page); + await homepage.visit(); + await booknowdatapicker.bookingpage(); + }); + + test("Select Check-in and Check-out Dates Using Datepicker", async ({ + page, + }) => { + await booknowdatapicker.booknowdatecheckin(page); + await booknowdatapicker.booknowdatecheckout(page); + await booknowdatapicker.checkavaialibilityclick(); + await booknowdatapicker.waitFirstCardReady(); + const firstValues = await booknowpage.viewandgetservicesfirstslide(); + await booknowpage.clickfirstroom(); + const secondValues = await booknowpage.viewandgetservicessecondslide(); + expect(secondValues.src).toBe(firstValues.src); + expect(secondValues.description).toBe(firstValues.description); + expect(secondValues.tv).toBe(firstValues.tv); + expect(secondValues.wifi).toBe(firstValues.wifi); + expect(secondValues.safe).toBe(firstValues.safe); + expect(secondValues.cost).toBe(firstValues.cost); + await booknowpage.reservearoomsuccess( + "Javier", + "Testing", + "javier@testing.com", + "31089485962", + page, + ); + }); +}); diff --git a/src/test/e2e/05.locations.spec.ts b/src/test/e2e/05.locations.spec.ts new file mode 100644 index 0000000..33c155d --- /dev/null +++ b/src/test/e2e/05.locations.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; +import { Location } from "../../pages/05.location"; +import { NavBar } from "../../pages/components/navbar"; + +test.describe("Locations Sectons", () => { + let homepage: Homepage; + let location: Location; + let navbar: NavBar; + + test.beforeEach(async ({ page }) => { + homepage = new Homepage(page); + location = new Location(page); + navbar = new NavBar(page); + console.log("toggler count:", await navbar.navbartoggler.count()); + console.log("toggler visible:", await navbar.navbartoggler.isVisible()); + console.log("collapse hidden:", await navbar.collapse.isHidden()); + await page.screenshot({ path: "pre-toggle.png" }); + await homepage.visit(); + }); + + test("Our Location", async ({ page }) => { + await navbar.clickontabtext("Location"); + await location.locationpagesection(); + await location.contactinformationtext(); + }); +}); diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-Mobile-Chrome-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-Mobile-Chrome-win32.png new file mode 100644 index 0000000..c49b575 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-Mobile-Chrome-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-Mobile-Safari-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-Mobile-Safari-win32.png new file mode 100644 index 0000000..ef36255 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-Mobile-Safari-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-chromium-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-chromium-win32.png new file mode 100644 index 0000000..8084468 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-chromium-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-firefox-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-firefox-win32.png new file mode 100644 index 0000000..168f578 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-firefox-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-webkit-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-webkit-win32.png new file mode 100644 index 0000000..e378143 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/contactinformationcard-webkit-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-Mobile-Chrome-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-Mobile-Chrome-win32.png new file mode 100644 index 0000000..45958c3 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-Mobile-Chrome-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-Mobile-Safari-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-Mobile-Safari-win32.png new file mode 100644 index 0000000..ef36255 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-Mobile-Safari-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-chromium-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-chromium-win32.png new file mode 100644 index 0000000..8084468 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-chromium-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-firefox-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-firefox-win32.png new file mode 100644 index 0000000..19e192a Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-firefox-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-webkit-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-webkit-win32.png new file mode 100644 index 0000000..e378143 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/locationsection-webkit-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-Mobile-Chrome-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-Mobile-Chrome-win32.png new file mode 100644 index 0000000..c49b575 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-Mobile-Chrome-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-Mobile-Safari-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-Mobile-Safari-win32.png new file mode 100644 index 0000000..ef36255 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-Mobile-Safari-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-chromium-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-chromium-win32.png new file mode 100644 index 0000000..8084468 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-chromium-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-firefox-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-firefox-win32.png new file mode 100644 index 0000000..19e192a Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-firefox-win32.png differ diff --git a/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-webkit-win32.png b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-webkit-win32.png new file mode 100644 index 0000000..e378143 Binary files /dev/null and b/src/test/e2e/05.locations.spec.ts-snapshots/maplocation-webkit-win32.png differ diff --git a/src/test/e2e/06.sendmessage.spec.ts b/src/test/e2e/06.sendmessage.spec.ts new file mode 100644 index 0000000..47f3369 --- /dev/null +++ b/src/test/e2e/06.sendmessage.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; +import { Sendmessage } from "../../pages/06.sendmessage"; +import { NavBar } from "../../pages/components/navbar"; + +test.describe("Locations Sectons", () => { + let homepage: Homepage; + let sendmessage: Sendmessage; + let navbar: NavBar; + + test.beforeEach(async ({ page }) => { + homepage = new Homepage(page); + sendmessage = new Sendmessage(page); + navbar = new NavBar(page); + + await homepage.visit(); + console.log("toggler count:", await navbar.navbartoggler.count()); + console.log("toggler visible:", await navbar.navbartoggler.isVisible()); + console.log("collapse hidden:", await navbar.collapse.isHidden()); + await page.screenshot({ path: "pre-toggle.png" }); + }); + + test("Send us message clean", async ({ page }) => { + await navbar.clickontabtext("Contact"); + await sendmessage.messagesectionclean(); + }); + test("Send us message Wrong", async ({ page }) => { + await navbar.clickontabtext("Contact"); + await sendmessage.messagesectionwrong(); + }); + test("Send us message Sucessfully", async ({ page }) => { + await navbar.clickontabtext("Contact"); + await sendmessage.messagesectionsucesfully(); + }); +}); diff --git a/test/example02.spec.ts b/src/test/e2e/example02.spec.ts similarity index 99% rename from test/example02.spec.ts rename to src/test/e2e/example02.spec.ts index e21225f..7d7f320 100644 --- a/test/example02.spec.ts +++ b/src/test/e2e/example02.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from "@playwright/test"; import { addMonths, addWeeks, addYears, format } from "date-fns"; +/* test("example test", async ({ page }) => { await page.goto("/"); @@ -17,3 +18,4 @@ test("example test", async ({ page }) => { await page.click(".react-datepicker__input-container"); await page.getByRole("option", { name: aria }).click(); }); +*/ diff --git a/src/test/visual/homepagevisual.spec.ts b/src/test/visual/homepagevisual.spec.ts new file mode 100644 index 0000000..dae605b --- /dev/null +++ b/src/test/visual/homepagevisual.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; +import { Homepage } from "../../pages/01.homepage"; + +test.describe.only("Homepage Visual Tests", () => { + let homepage: Homepage; + + test.beforeEach(async ({ page }) => { + homepage = new Homepage(page); + await homepage.visit(); + }); + + test("Container Visual Test", async (page) => { + await homepage.containersnapshot(); + }); + test("Card Body Visual Test", async () => { + await homepage.cardbodysnapshot(); + }); + test("Rooms Section Visual Test", async () => { + await homepage.roomssnapshot(); + }); + test("Location Section Visual Test", async () => { + await homepage.locationsnapshot(); + }); + test("Contact Section Visual Test", async () => { + await homepage.contactsnapshot(); + }); +}); diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-Mobile-Chrome-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-Mobile-Chrome-win32.png new file mode 100644 index 0000000..690bac5 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-Mobile-Chrome-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-Mobile-Safari-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-Mobile-Safari-win32.png new file mode 100644 index 0000000..aba6331 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-Mobile-Safari-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-chromium-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-chromium-win32.png new file mode 100644 index 0000000..6140fda Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-chromium-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-firefox-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-firefox-win32.png new file mode 100644 index 0000000..493c32a Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-firefox-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-webkit-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-webkit-win32.png new file mode 100644 index 0000000..d25647c Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/cardbody-webkit-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/contact-Mobile-Chrome-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-Mobile-Chrome-win32.png new file mode 100644 index 0000000..36fff13 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-Mobile-Chrome-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/contact-Mobile-Safari-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-Mobile-Safari-win32.png new file mode 100644 index 0000000..b2c3e4e Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-Mobile-Safari-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/contact-chromium-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-chromium-win32.png new file mode 100644 index 0000000..f22c6bc Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-chromium-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/contact-firefox-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-firefox-win32.png new file mode 100644 index 0000000..dc0ec10 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-firefox-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/contact-webkit-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-webkit-win32.png new file mode 100644 index 0000000..9dde18f Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/contact-webkit-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-Mobile-Chrome-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-Mobile-Chrome-win32.png new file mode 100644 index 0000000..137eda1 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-Mobile-Chrome-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-Mobile-Safari-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-Mobile-Safari-win32.png new file mode 100644 index 0000000..63672a4 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-Mobile-Safari-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-chromium-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-chromium-win32.png new file mode 100644 index 0000000..18e7779 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-chromium-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-firefox-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-firefox-win32.png new file mode 100644 index 0000000..6359ed2 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-firefox-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-webkit-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-webkit-win32.png new file mode 100644 index 0000000..93f9a14 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/homepage-webkit-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/location-Mobile-Chrome-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/location-Mobile-Chrome-win32.png new file mode 100644 index 0000000..6a57177 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/location-Mobile-Chrome-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/location-Mobile-Safari-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/location-Mobile-Safari-win32.png new file mode 100644 index 0000000..89d0a97 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/location-Mobile-Safari-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/location-chromium-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/location-chromium-win32.png new file mode 100644 index 0000000..17b163b Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/location-chromium-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/location-firefox-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/location-firefox-win32.png new file mode 100644 index 0000000..0a05457 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/location-firefox-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/location-webkit-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/location-webkit-win32.png new file mode 100644 index 0000000..8060833 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/location-webkit-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-Mobile-Chrome-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-Mobile-Chrome-win32.png new file mode 100644 index 0000000..918b31d Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-Mobile-Chrome-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-Mobile-Safari-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-Mobile-Safari-win32.png new file mode 100644 index 0000000..dc08614 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-Mobile-Safari-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-chromium-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-chromium-win32.png new file mode 100644 index 0000000..dd74487 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-chromium-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-firefox-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-firefox-win32.png new file mode 100644 index 0000000..ffc3414 Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-firefox-win32.png differ diff --git a/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-webkit-win32.png b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-webkit-win32.png new file mode 100644 index 0000000..ca393ed Binary files /dev/null and b/src/test/visual/homepagevisual.spec.ts-snapshots/rooms-webkit-win32.png differ diff --git a/src/utils/textutils.ts b/src/utils/textutils.ts new file mode 100644 index 0000000..b5856f5 --- /dev/null +++ b/src/utils/textutils.ts @@ -0,0 +1,7 @@ +export function normalizeText(text: string | null): string { + if (!text) return ""; + return text + .replace(/\s+/g, " ") // junta múltiples espacios/saltos en 1 espacio + .replace(/['"]+/g, "") // quita comillas ' o " + .trim(); // corta espacios al inicio y fin +} diff --git a/summary.html b/summary.html new file mode 100644 index 0000000..6902ff9 --- /dev/null +++ b/summary.html @@ -0,0 +1,861 @@ + + + + + + + + + + + + + Test Report: 2025-10-07 19:35 + + + + + +
+
+

+ + Test Report: 2025-10-07 19:35 +

+
+ +
+ +
+
+ +

Total Requests

+
+ 11656 + +
+
+ + +
+ +

Failed Requests

+
2544
+
+ + +
+ +

Breached Thresholds

+
1
+
+ +
+ +

Failed Checks

+
0
+
+
+ + +
+ + +
+ + +

Trends & Times

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.550.000.00311.960.000.00
http_req_connecting0.170.000.0074.880.000.00
http_req_duration80.3061.1067.996063.3183.1197.57
http_req_receiving0.090.000.006.590.501.00
http_req_sending0.000.000.004.030.000.00
http_req_tls_handshaking0.350.000.00148.710.000.00
http_req_waiting80.2161.1067.916063.3182.9397.46
iteration_duration5407.115316.625346.4611411.775446.625493.17
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + +
RatePassesFails
http_req_failed0.222544.009112.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + +
Count
dropped_iterations69.00
+ + + + +
+ + + + +
+
+ +
+

Checks

+ +
+ Passed + 0 +
+
+ Failed + 0 +
+
+ + + +
+

Iterations

+ +
+ Total + 2331 +
+
+ Rate + 3.21/s +
+
+ + +
+

Virtual Users

+ +
+ Min + 2 +
+
+ Max + 28 +
+
+ +
+

Requests

+ +
+ Total + + 11656 + + +
+
+ Rate + + 16.06/s + + +
+
+ +
+

Data Received

+ +
+ Total + 10.22 MB +
+
+ Rate + 0.01 mB/s +
+
+ +
+

Data Sent

+ +
+ Total + 3.24 MB +
+
+ Rate + 0.00 mB/s +
+
+
+
+ + + + +
+ + + + +

Other Checks

+ + + + + + + + + + + +
Check NamePassesFailures
+
+ +
+
+ + +
+ + diff --git a/summary.json b/summary.json new file mode 100644 index 0000000..cb8cbd8 --- /dev/null +++ b/summary.json @@ -0,0 +1,231 @@ +{ + "options": { + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(90)", + "p(95)" + ], + "summaryTimeUnit": "", + "noColor": false + }, + "state": { + "isStdOutTTY": true, + "isStdErrTTY": true, + "testRunDurationMs": 725635.7477 + }, + "metrics": { + "checks": { + "type": "rate", + "contains": "default", + "values": { + "rate": 0, + "passes": 0, + "fails": 0 + }, + "thresholds": { + "rate>0.99": { + "ok": true + } + } + }, + "http_req_duration": { + "thresholds": { + "p(95)<800": { + "ok": true + } + }, + "type": "trend", + "contains": "time", + "values": { + "max": 6063.31, + "p(90)": 83.1126, + "p(95)": 97.572875, + "avg": 80.30343612731622, + "min": 61.1018, + "med": 67.99334999999999 + } + }, + "iteration_duration": { + "values": { + "avg": 5407.105398755899, + "min": 5316.6191, + "med": 5346.4579, + "max": 11411.7741, + "p(90)": 5446.624, + "p(95)": 5493.1749 + }, + "type": "trend", + "contains": "time" + }, + "http_req_blocked": { + "type": "trend", + "contains": "time", + "values": { + "p(95)": 0, + "avg": 0.5461088366506521, + "min": 0, + "med": 0, + "max": 311.956, + "p(90)": 0 + } + }, + "http_req_failed": { + "type": "rate", + "contains": "default", + "values": { + "rate": 0.2182566918325326, + "passes": 2544, + "fails": 9112 + }, + "thresholds": { + "rate<0.01": { + "ok": false + } + } + }, + "http_reqs": { + "contains": "default", + "values": { + "rate": 16.063155704422304, + "count": 11656 + }, + "type": "counter" + }, + "http_req_sending": { + "type": "trend", + "contains": "time", + "values": { + "max": 4.026, + "p(90)": 0, + "p(95)": 0, + "avg": 0.0032049330816746736, + "min": 0, + "med": 0 + } + }, + "http_req_connecting": { + "type": "trend", + "contains": "time", + "values": { + "min": 0, + "med": 0, + "max": 74.8836, + "p(90)": 0, + "p(95)": 0, + "avg": 0.17451260295126975 + } + }, + "http_req_tls_handshaking": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.3539949639670555, + "min": 0, + "med": 0, + "max": 148.7114, + "p(90)": 0, + "p(95)": 0 + } + }, + "http_req_waiting": { + "type": "trend", + "contains": "time", + "values": { + "avg": 80.2110261839394, + "min": 61.1018, + "med": 67.91435, + "max": 6063.31, + "p(90)": 82.93295, + "p(95)": 97.455375 + } + }, + "data_received": { + "type": "counter", + "contains": "data", + "values": { + "count": 10216976, + "rate": 14080.034001059179 + } + }, + "iterations": { + "type": "counter", + "contains": "default", + "values": { + "count": 2331, + "rate": 3.2123555205051813 + } + }, + "vus_max": { + "values": { + "max": 30, + "value": 30, + "min": 15 + }, + "type": "gauge", + "contains": "default" + }, + "http_req_receiving": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.0892050102951271, + "min": 0, + "med": 0, + "max": 6.5894, + "p(90)": 0.504, + "p(95)": 1.0012 + } + }, + "vus": { + "values": { + "value": 2, + "min": 2, + "max": 28 + }, + "type": "gauge", + "contains": "default" + }, + "data_sent": { + "type": "counter", + "contains": "data", + "values": { + "count": 3241542, + "rate": 4467.175177455772 + } + }, + "http_req_duration{expected_response:true}": { + "type": "trend", + "contains": "time", + "values": { + "avg": 77.93754563213334, + "min": 61.6475, + "med": 68.18015, + "max": 5069.9866, + "p(90)": 78.47153999999998, + "p(95)": 93.64054 + } + }, + "dropped_iterations": { + "contains": "default", + "values": { + "rate": 0.09508903085150472, + "count": 69 + }, + "type": "counter" + } + }, + "setup_data": { + "baseurl": "https://restful-booker.herokuapp.com", + "token": "51c30795daf006d" + }, + "root_group": { + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": [], + "checks": [] + } +} \ No newline at end of file diff --git a/test/example01.spec.ts b/test/example01.spec.ts deleted file mode 100644 index 210ce28..0000000 --- a/test/example01.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("basic test", async ({ page }) => { - await page.goto("/"); - const title = page.locator("h1"); - await expect(title).toHaveText("Welcome to Shady Meadows B&B"); -}); diff --git a/test/example03.spec.ts b/test/example03.spec.ts deleted file mode 100644 index 561f7e2..0000000 --- a/test/example03.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { addMonths, addWeeks, addYears, format } from "date-fns"; - -test.describe("Example Test Suite", () => { - test("checkIn", async ({ page }) => { - await page.goto("/"); - const inputs = page.locator(".react-datepicker__input-container input"); - - const today = new Date(); - const nextWeek = new Date(today); - nextWeek.setDate(today.getDate() + 7); - - const weekday = nextWeek.toLocaleDateString("en-US", { weekday: "long" }); - const day = nextWeek.getDate(); - const month = nextWeek.toLocaleDateString("en-US", { month: "long" }); - const year = nextWeek.getFullYear(); - const aria = `Choose ${weekday}, ${day} ${month} ${year}`; - - await inputs.nth(0).click(); - await page.getByRole("option", { name: aria }).click(); - }); - - test("checkOut", async ({ page }) => { - await page.goto("/"); - const inputs = page.locator(".react-datepicker__input-container input"); - - const today = new Date(); - const nextWeek = new Date(today); - nextWeek.setDate(today.getDate() + 15); - - const weekday = nextWeek.toLocaleDateString("en-US", { weekday: "long" }); - const day = nextWeek.getDate(); - const month = nextWeek.toLocaleDateString("en-US", { month: "long" }); - const year = nextWeek.getFullYear(); - const aria = `Choose ${weekday}, ${day} ${month} ${year}`; - - await inputs.nth(1).click(); - await page.getByRole("option", { name: aria }).click(); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 938b604..61f29e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { "target": "ESNext", - "module": "CommonJS", - "moduleResolution": "Node", - "types": ["node", "@playwright/test"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "moduleDetection": "force", "strict": true, "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["@playwright/test", "node"] }, - "include": ["tests/**/*.ts", "playwright.config.ts"] + "include": ["src/**/*.ts", "playwright.config.ts"] } diff --git a/visual.config.ts b/visual.config.ts new file mode 100644 index 0000000..79a0e30 --- /dev/null +++ b/visual.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from "@playwright/test"; +import "dotenv/config"; + +const isCI = !!process.env.CI; + +export default defineConfig({ + testDir: "./src/test/visual", + + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: isCI + ? [["html", { open: "never" }], ["list"]] + : [["html", { open: "never" }]], + + expect: { + timeout: 5_000, + }, + + use: { + baseURL: process.env.BASE_URL || "https://automationintesting.online", + headless: true, + actionTimeout: 0, + navigationTimeout: 30_000, + trace: "on-first-retry", + video: "off", + screenshot: "off", + }, + + projects: [ + // Desktop + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + + // Mobile emulation + { name: "Mobile Chrome", use: { ...devices["Pixel 7"] } }, + { name: "Mobile Safari", use: { ...devices["iPhone 14"] } }, + ], +});