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
+
+
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ http_req_blocked |
+
+ 0.55 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 311.96 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ http_req_connecting |
+
+ 0.17 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 74.88 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ http_req_duration |
+
+ 80.30 |
+
+ 61.10 |
+
+ 67.99 |
+
+ 6063.31 |
+
+ 83.11 |
+
+ 97.57 |
+
+
+
+
+ http_req_receiving |
+
+ 0.09 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 6.59 |
+
+ 0.50 |
+
+ 1.00 |
+
+
+
+
+ http_req_sending |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 4.03 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ http_req_tls_handshaking |
+
+ 0.35 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 148.71 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ http_req_waiting |
+
+ 80.21 |
+
+ 61.10 |
+
+ 67.91 |
+
+ 6063.31 |
+
+ 82.93 |
+
+ 97.46 |
+
+
+
+
+ iteration_duration |
+
+ 5407.11 |
+
+ 5316.62 |
+
+ 5346.46 |
+
+ 11411.77 |
+
+ 5446.62 |
+
+ 5493.17 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate |
+ Passes |
+ Fails |
+
+
+
+
+
+ http_req_failed |
+
+ 0.22 |
+
+ 2544.00 |
+
+ 9112.00 |
+
+
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ dropped_iterations |
+
+ 69.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 Name |
+ Passes |
+ Failures |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"] } },
+ ],
+});