diff --git a/examples/multiProject/cucumberjs/features/multiProject.feature b/examples/multiProject/cucumberjs/features/multiProject.feature index 66efc67a..33eaf766 100644 --- a/examples/multiProject/cucumberjs/features/multiProject.feature +++ b/examples/multiProject/cucumberjs/features/multiProject.feature @@ -1,14 +1,43 @@ -Feature: Multi-project example - Scenarios for testops_multi mode. Use @qaseid.PROJ(ids) tags for multi-project. +Feature: Multi-project API - User Operations + API CRUD operations reported to multiple Qase projects. + Use @qaseid.PROJ(ids) tags for multi-project reporting. - Scenario: Scenario without Qase ID - Given I have a step + Background: + Given the API is available at "https://jsonplaceholder.typicode.com" @qaseid.PROJ1(1) @qaseid.PROJ2(2) - Scenario: Scenario reported to two projects - Given I have a step + Scenario: Get all users returns 10 users + When I send a GET request to "/users" + Then the response status should be 200 + And the response should contain 10 items + And each item should have an "id" field + And each item should have an "email" field - @QaseID=3 - Scenario: Scenario with legacy single-project tag - Given I have a step + @qaseid.PROJ1(3) + @qaseid.PROJ2(4) + Scenario: Get single user by ID returns correct user + When I send a GET request to "/users/1" + Then the response status should be 200 + And the response "name" should be "Leanne Graham" + And the response "email" should be "Sincere@april.biz" + + @qaseid.PROJ1(5) + @qaseid.PROJ2(6) + Scenario: Create new user returns 201 with ID + When I send a POST request to "/users" with body: + """ + { + "name": "Test User", + "username": "testuser", + "email": "test@example.com" + } + """ + Then the response status should be 201 + And the response should have an "id" field + + @qaseid.PROJ1(7) + @qaseid.PROJ2(8) + Scenario: Delete user returns 200 status + When I send a DELETE request to "/users/1" + Then the response status should be 200 diff --git a/examples/multiProject/cucumberjs/step_definitions/steps.js b/examples/multiProject/cucumberjs/step_definitions/steps.js index 454de0d4..e9799ee7 100644 --- a/examples/multiProject/cucumberjs/step_definitions/steps.js +++ b/examples/multiProject/cucumberjs/step_definitions/steps.js @@ -1,6 +1,77 @@ -const { Given } = require('@cucumber/cucumber'); +const { Given, When, Then } = require('@cucumber/cucumber'); +const assert = require('assert'); -Given('I have a step', function () { - console.log('I have a step'); - this.attach("I'm an attachment", 'text/plain'); +Given('the API is available at {string}', function (baseUrl) { + this.baseUrl = baseUrl; +}); + +When('I send a GET request to {string}', async function (endpoint) { + const url = `${this.baseUrl}${endpoint}`; + this.response = await fetch(url); + this.responseData = await this.response.json(); + + this.attach(JSON.stringify({ + url: url, + status: this.response.status, + body: Array.isArray(this.responseData) + ? `[${this.responseData.length} items]` + : this.responseData, + }, null, 2), 'application/json'); +}); + +When('I send a POST request to {string} with body:', async function (endpoint, docString) { + const url = `${this.baseUrl}${endpoint}`; + this.response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: docString, + }); + this.responseData = await this.response.json(); + + this.attach(JSON.stringify({ + request: { url, method: 'POST', body: JSON.parse(docString) }, + response: { status: this.response.status, body: this.responseData }, + }, null, 2), 'application/json'); +}); + +When('I send a DELETE request to {string}', async function (endpoint) { + const url = `${this.baseUrl}${endpoint}`; + this.response = await fetch(url, { method: 'DELETE' }); + this.responseData = await this.response.json(); + + this.attach(JSON.stringify({ + url: url, + method: 'DELETE', + status: this.response.status, + }, null, 2), 'application/json'); +}); + +Then('the response status should be {int}', function (expectedStatus) { + assert.strictEqual(this.response.status, expectedStatus, + `Expected status ${expectedStatus} but got ${this.response.status}`); +}); + +Then('the response should contain {int} items', function (count) { + assert.ok(Array.isArray(this.responseData), + `Expected response to be an array but got ${typeof this.responseData}`); + assert.strictEqual(this.responseData.length, count, + `Expected ${count} items but got ${this.responseData.length}`); +}); + +Then('each item should have an {string} field', function (fieldName) { + assert.ok(Array.isArray(this.responseData), 'Response should be an array'); + for (const item of this.responseData) { + assert.ok(item[fieldName] !== undefined, + `Item missing required field: ${fieldName}`); + } +}); + +Then('the response {string} should be {string}', function (field, expected) { + assert.strictEqual(String(this.responseData[field]), expected, + `Expected ${field} to be "${expected}" but got "${this.responseData[field]}"`); +}); + +Then('the response should have an {string} field', function (fieldName) { + assert.ok(this.responseData[fieldName] !== undefined, + `Response missing required field: ${fieldName}`); }); diff --git a/examples/multiProject/cypress/cypress/e2e/multiProject.cy.js b/examples/multiProject/cypress/cypress/e2e/multiProject.cy.js index b4226114..84164482 100644 --- a/examples/multiProject/cypress/cypress/e2e/multiProject.cy.js +++ b/examples/multiProject/cypress/cypress/e2e/multiProject.cy.js @@ -1,19 +1,145 @@ const { qase } = require('cypress-qase-reporter/mocha'); -describe('Multi-project example', () => { - // Map this test to case 1 in PROJ1 and case 2 in PROJ2. Replace IDs with real case IDs in your projects. - qase.projects({ PROJ1: [1], PROJ2: [2] }, it('A test reported to two projects', () => { - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - })); - - qase.projects( - { PROJ1: [10, 11], PROJ2: [20] }, - it('Another test with multiple cases per project', () => { - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); +describe('Multi-project Login Scenarios', () => { + beforeEach(() => { + cy.visit('https://www.saucedemo.com'); + }); + + // Report to PROJ1 (case 1) and PROJ2 (case 2) + qase.projects({ PROJ1: [1], PROJ2: [2] }, + it('User can login with valid credentials', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + + qase.step('Fill in credentials and submit', () => { + cy.get('[data-test="username"]').type('standard_user'); + cy.get('[data-test="password"]').type('secret_sauce'); + cy.get('[data-test="login-button"]').click(); + }); + + qase.step('Verify successful login', () => { + cy.url().should('include', '/inventory.html'); + cy.get('[data-test="title"]').should('have.text', 'Products'); + }); + + qase.comment('Login successful — reported to PROJ1 and PROJ2'); + }), + ); + + // Report to PROJ1 (case 3) and PROJ2 (case 4) + qase.projects({ PROJ1: [3], PROJ2: [4] }, + it('Invalid password shows error', () => { + qase.fields({ severity: 'high', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'standard_user', password: 'wrong_password' }); + + qase.step('Attempt login with invalid credentials', () => { + cy.get('[data-test="username"]').type('standard_user'); + cy.get('[data-test="password"]').type('wrong_password'); + cy.get('[data-test="login-button"]').click(); + }); + + qase.step('Verify error message', () => { + cy.get('[data-test="error"]') + .should('be.visible') + .and('contain.text', 'Username and password do not match'); + }); + + qase.comment('Error correctly displayed — tracked in both projects'); + }), + ); + + // Report to PROJ1 (case 5) and PROJ2 (case 6) + qase.projects({ PROJ1: [5], PROJ2: [6] }, + it('Locked user cannot login', () => { + qase.fields({ severity: 'high', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'locked_out_user' }); + + qase.step('Attempt login with locked user', () => { + cy.get('[data-test="username"]').type('locked_out_user'); + cy.get('[data-test="password"]').type('secret_sauce'); + cy.get('[data-test="login-button"]').click(); + }); + + qase.step('Verify locked-out error', () => { + cy.get('[data-test="error"]') + .should('be.visible') + .and('contain.text', 'Sorry, this user has been locked out'); + }); + }), + ); +}); + +describe('Multi-project Cart Scenarios', () => { + beforeEach(() => { + cy.visit('https://www.saucedemo.com'); + cy.get('[data-test="username"]').type('standard_user'); + cy.get('[data-test="password"]').type('secret_sauce'); + cy.get('[data-test="login-button"]').click(); + cy.url().should('include', '/inventory.html'); + }); + + // Report to PROJ1 (cases 7, 8) and PROJ2 (case 9) + qase.projects({ PROJ1: [7, 8], PROJ2: [9] }, + it('Add product to cart', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCart\tAdd Items'); + qase.parameters({ product: 'Sauce Labs Backpack' }); + + qase.step('Add Sauce Labs Backpack to cart', () => { + cy.get('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + }); + + qase.step('Verify cart badge shows 1 item', () => { + cy.get('.shopping_cart_badge').should('have.text', '1'); + }); + + qase.step('Navigate to cart and verify product', () => { + cy.get('.shopping_cart_link').click(); + cy.get('.inventory_item_name').should('contain.text', 'Backpack'); + }); + + qase.comment('Product added to cart — reported to both projects'); + + qase.attach({ + name: 'cart-state.json', + content: JSON.stringify({ + product: 'Sauce Labs Backpack', + quantity: 1, + projects: ['PROJ1', 'PROJ2'], + }, null, 2), + contentType: 'application/json', + }); + }), + ); + + // Report to PROJ1 (case 10) and PROJ2 (case 11) + qase.projects({ PROJ1: [10], PROJ2: [11] }, + it('Complete checkout flow', () => { + qase.fields({ severity: 'critical', priority: 'critical', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + qase.step('Add product and go to checkout', () => { + cy.get('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + cy.get('.shopping_cart_link').click(); + cy.get('[data-test="checkout"]').click(); + }); + + qase.step('Fill checkout information', () => { + cy.get('[data-test="firstName"]').type('John'); + cy.get('[data-test="lastName"]').type('Doe'); + cy.get('[data-test="postalCode"]').type('12345'); + }); + + qase.step('Complete and verify order', () => { + cy.get('[data-test="continue"]').click(); + cy.get('[data-test="finish"]').click(); + cy.get('.complete-header').should('contain.text', 'Thank you'); + }); + + qase.comment('Full checkout flow verified across both projects'); }), ); }); diff --git a/examples/multiProject/cypressBadeballCucumber/cypress/e2e/multiProject.feature b/examples/multiProject/cypressBadeballCucumber/cypress/e2e/multiProject.feature index 00c6cbd3..bdc6c077 100644 --- a/examples/multiProject/cypressBadeballCucumber/cypress/e2e/multiProject.feature +++ b/examples/multiProject/cypressBadeballCucumber/cypress/e2e/multiProject.feature @@ -1,16 +1,22 @@ Feature: Multi-project tests with @badeball/cypress-cucumber-preprocessor - # Multi-project: case 1 in PROJ1, case 2 in PROJ2. Use @qaseid.PROJECT(ids) format. @qaseid.PROJ1(1) @qaseid.PROJ2(2) - Scenario: test reported to two projects + Scenario: Homepage navigation reported to two projects Given I am on the homepage When I click on the first link Then I should see the first link - @qaseid.PROJ1(10,11) - @qaseid.PROJ2(20) - Scenario: test with multiple cases per project + @qaseid.PROJ1(3) + @qaseid.PROJ2(4) + Scenario: Homepage navigation with expected failure + Given I am on the homepage + When I should see the first link failed + Then I should see the first link + + @qaseid.PROJ1(5,6) + @qaseid.PROJ2(7) + Scenario: Multiple case IDs across projects Given I am on the homepage When I click on the first link Then I should see the first link diff --git a/examples/multiProject/cypressCucumber/cypress/e2e/multiProject.feature b/examples/multiProject/cypressCucumber/cypress/e2e/multiProject.feature index 25bc69f6..e9237f07 100644 --- a/examples/multiProject/cypressCucumber/cypress/e2e/multiProject.feature +++ b/examples/multiProject/cypressCucumber/cypress/e2e/multiProject.feature @@ -2,14 +2,21 @@ Feature: Multi-project tests with cypress-cucumber-preprocessor @qaseid.PROJ1(1) @qaseid.PROJ2(2) - Scenario: test reported to two projects + Scenario: Homepage navigation reported to two projects Given I am on the homepage When I click on the first link Then I should see the first link - @qaseid.PROJ1(10,11) - @qaseid.PROJ2(20) - Scenario: test with multiple cases per project + @qaseid.PROJ1(3) + @qaseid.PROJ2(4) + Scenario: Homepage navigation with expected failure + Given I am on the homepage + When I should see the first link failed + Then I should see the first link + + @qaseid.PROJ1(5,6) + @qaseid.PROJ2(7) + Scenario: Multiple case IDs across projects Given I am on the homepage When I click on the first link Then I should see the first link diff --git a/examples/multiProject/jest/test/multiProject.test.js b/examples/multiProject/jest/test/multiProject.test.js index 12c67be2..cca72bc1 100644 --- a/examples/multiProject/jest/test/multiProject.test.js +++ b/examples/multiProject/jest/test/multiProject.test.js @@ -1,19 +1,105 @@ const { qase } = require('jest-qase-reporter/jest'); -const { describe, test, expect } = require('@jest/globals'); +const { expect } = require('@jest/globals'); -describe('Multi-project example', () => { - // Map this test to case 1 in PROJ1 and case 2 in PROJ2. Replace IDs with real case IDs in your projects. - test(qase.projects({ PROJ1: [1], PROJ2: [2] }, 'A test reported to two projects'), () => { - expect(true).toBe(true); +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +describe('Multi-project API - User Operations', () => { + // Report to PROJ1 (case 1) and PROJ2 (case 2) + test(qase.projects({ PROJ1: [1], PROJ2: [2] }, 'GET all users returns 10 users'), async () => { + qase.fields({ layer: 'api', severity: 'normal', priority: 'high' }); + + await qase.step('Send GET request to /users endpoint', async () => { + const response = await fetch(`${BASE_URL}/users`); + expect(response.status).toBe(200); + + const users = await response.json(); + expect(Array.isArray(users)).toBe(true); + expect(users.length).toBe(10); + }); + + await qase.step('Verify response contains valid user structure', async () => { + const response = await fetch(`${BASE_URL}/users`); + const users = await response.json(); + const firstUser = users[0]; + + expect(firstUser).toHaveProperty('id'); + expect(firstUser).toHaveProperty('name'); + expect(firstUser).toHaveProperty('email'); + }); + + qase.comment('All users returned successfully — reported to PROJ1 and PROJ2'); + }); + + // Report to PROJ1 (case 3) and PROJ2 (case 4) + test(qase.projects({ PROJ1: [3], PROJ2: [4] }, 'GET single user by ID returns correct user'), async () => { + qase.parameters({ userId: 1 }); + qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step('Send GET request to /users/1', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user.id).toBe(1); + expect(user.name).toBe('Leanne Graham'); + expect(user.email).toBe('Sincere@april.biz'); + }); + + qase.comment('User details verified — tracked across both projects'); + }); + + // Report to PROJ1 (case 5) and PROJ2 (case 6) + test(qase.projects({ PROJ1: [5], PROJ2: [6] }, 'POST create new user returns 201 with ID'), async () => { + qase.fields({ layer: 'api', severity: 'critical', priority: 'high' }); + + const newUser = { + name: 'Test User', + username: 'testuser', + email: 'test@example.com', + }; + + await qase.step('Send POST request to create user', async () => { + const response = await fetch(`${BASE_URL}/users`, { + method: 'POST', + body: JSON.stringify(newUser), + headers: { 'Content-Type': 'application/json' }, + }); + + expect(response.status).toBe(201); + + const createdUser = await response.json(); + expect(createdUser).toHaveProperty('id'); + + qase.attach({ + name: 'request-body.json', + content: JSON.stringify(newUser, null, 2), + contentType: 'application/json', + }); + }); + + qase.comment('User created — reported to PROJ1 and PROJ2'); }); - test( - qase.projects( - { PROJ1: [10, 11], PROJ2: [20] }, - 'Another test with multiple cases per project', - ), - () => { - expect(1 + 1).toBe(2); - }, - ); + // Report to PROJ1 (case 7) and PROJ2 (case 8) + test(qase.projects({ PROJ1: [7], PROJ2: [8] }, 'DELETE user returns 200 status'), async () => { + qase.fields({ layer: 'api', severity: 'normal' }); + qase.comment('Note: JSONPlaceholder fakes DELETE — no actual deletion occurs'); + + await qase.step('Send DELETE request to /users/1', async () => { + const response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + + expect(response.status).toBe(200); + }); + + await qase.step('Verify response is empty object', async () => { + const response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + + const result = await response.json(); + expect(result).toEqual({}); + }); + }); }); diff --git a/examples/multiProject/mocha/test/multiProject.spec.js b/examples/multiProject/mocha/test/multiProject.spec.js index 18477df8..710b023e 100644 --- a/examples/multiProject/mocha/test/multiProject.spec.js +++ b/examples/multiProject/mocha/test/multiProject.spec.js @@ -1,19 +1,132 @@ const assert = require('assert'); const { qase } = require('mocha-qase-reporter/mocha'); -describe('Multi-project example', function () { - // Map this test to case 1 in PROJ1 and case 2 in PROJ2. Replace IDs with real case IDs in your projects. - it(qase.projects({ PROJ1: [1], PROJ2: [2] }, 'A test reported to two projects'), function () { - assert.strictEqual(1, 1); +describe('Multi-project API - User Operations', function () { + const BASE_URL = 'https://jsonplaceholder.typicode.com'; + + // Report to PROJ1 (case 1) and PROJ2 (case 2) + it(qase.projects({ PROJ1: [1], PROJ2: [2] }, 'GET all users returns 10 users'), async function () { + qase.fields({ layer: 'api', severity: 'normal', priority: 'high' }); + + let response; + let users; + + await qase.step('Send GET request to /users endpoint', async () => { + response = await fetch(`${BASE_URL}/users`); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200, 'Expected status code 200'); + }); + + await qase.step('Parse and verify user list', async () => { + users = await response.json(); + assert.strictEqual(Array.isArray(users), true, 'Response should be an array'); + assert.strictEqual(users.length, 10, 'Should have exactly 10 users'); + }); + + await qase.step('Verify user structure has required fields', async () => { + const firstUser = users[0]; + assert.ok(firstUser.id, 'User should have id'); + assert.ok(firstUser.name, 'User should have name'); + assert.ok(firstUser.email, 'User should have email'); + }); + + qase.comment('All users returned successfully — reported to PROJ1 and PROJ2'); + }); + + // Report to PROJ1 (case 3) and PROJ2 (case 4) + it(qase.projects({ PROJ1: [3], PROJ2: [4] }, 'GET single user by ID returns correct user'), async function () { + qase.parameters({ userId: 1 }); + qase.fields({ layer: 'api', severity: 'normal' }); + + let response; + let user; + + await qase.step('Send GET request to /users/1', async () => { + response = await fetch(`${BASE_URL}/users/1`); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200); + }); + + await qase.step('Verify user is Leanne Graham', async () => { + user = await response.json(); + assert.strictEqual(user.id, 1, 'User ID should be 1'); + assert.strictEqual(user.name, 'Leanne Graham'); + assert.strictEqual(user.email, 'Sincere@april.biz'); + }); + + await qase.step('Verify user has address and company details', async () => { + assert.ok(user.address, 'User should have address'); + assert.ok(user.company, 'User should have company'); + }); + + qase.comment('User details verified — tracked across both projects'); }); - it( - qase.projects( - { PROJ1: [10, 11], PROJ2: [20] }, - 'Another test with multiple cases per project', - ), - function () { - assert.strictEqual(1 + 1, 2); - }, - ); + // Report to PROJ1 (case 5) and PROJ2 (case 6) + it(qase.projects({ PROJ1: [5], PROJ2: [6] }, 'POST create new user returns 201 with ID'), async function () { + qase.fields({ layer: 'api', severity: 'critical', priority: 'high' }); + + const newUser = { + name: 'Test User', + username: 'testuser', + email: 'test@example.com', + }; + + let response; + let createdUser; + + await qase.step('Send POST request to create user', async () => { + response = await fetch(`${BASE_URL}/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newUser), + }); + }); + + await qase.step('Verify response status is 201 (Created)', async () => { + assert.strictEqual(response.status, 201, 'Expected status code 201 for created resource'); + }); + + await qase.step('Verify returned user has ID and matches request data', async () => { + createdUser = await response.json(); + assert.ok(createdUser.id, 'Created user should have an ID'); + assert.strictEqual(createdUser.name, newUser.name, 'Name should match'); + assert.strictEqual(createdUser.email, newUser.email, 'Email should match'); + }); + + qase.attach({ + name: 'request-body.json', + content: JSON.stringify(newUser, null, 2), + contentType: 'application/json', + }); + + qase.comment('User created — reported to PROJ1 and PROJ2'); + }); + + // Report to PROJ1 (case 7) and PROJ2 (case 8) + it(qase.projects({ PROJ1: [7], PROJ2: [8] }, 'DELETE user returns 200 status'), async function () { + qase.fields({ layer: 'api', severity: 'normal' }); + qase.comment('Note: JSONPlaceholder fakes DELETE — no actual deletion occurs'); + + let response; + + await qase.step('Send DELETE request for user ID 1', async () => { + response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200, 'DELETE should return 200 status'); + }); + + await qase.step('Verify empty response body', async () => { + const body = await response.json(); + assert.strictEqual(typeof body, 'object', 'Response should be an object'); + }); + }); }); diff --git a/examples/multiProject/playwright/test/multiProject.spec.js b/examples/multiProject/playwright/test/multiProject.spec.js index b9d80084..0630d157 100644 --- a/examples/multiProject/playwright/test/multiProject.spec.js +++ b/examples/multiProject/playwright/test/multiProject.spec.js @@ -1,29 +1,161 @@ const { test, expect } = require('@playwright/test'); const { qase } = require('playwright-qase-reporter'); -test.describe('Multi-project example', () => { - // 1) Via qase.projects() inside the test (metadata) - test('A test reported to two projects', async () => { +test.describe('Multi-project Login Scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('https://www.saucedemo.com'); + }); + + // Report to PROJ1 (case 1) and PROJ2 (case 2) + test('User can login with valid credentials', async ({ page }) => { qase.projects({ PROJ1: [1], PROJ2: [2] }); - expect(true).toBe(true); + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + + await test.step('Fill in credentials and submit', async () => { + await page.fill('[data-test="username"]', 'standard_user'); + await page.fill('[data-test="password"]', 'secret_sauce'); + await page.click('[data-test="login-button"]'); + }); + + await test.step('Verify successful login', async () => { + await expect(page).toHaveURL(/.*inventory.html/); + qase.comment('Login successful — reported to PROJ1 and PROJ2'); + }); }); - test('Another test with multiple cases per project', async () => { - qase.projects({ PROJ1: [10, 11], PROJ2: [20] }); - expect(1 + 1).toBe(2); + // Report to PROJ1 (case 3) and PROJ2 (case 4) + test('Invalid password shows error', async ({ page }) => { + qase.projects({ PROJ1: [3], PROJ2: [4] }); + qase.fields({ severity: 'high', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'standard_user', password: 'wrong_password' }); + + await test.step('Attempt login with invalid password', async () => { + await page.fill('[data-test="username"]', 'standard_user'); + await page.fill('[data-test="password"]', 'wrong_password'); + await page.click('[data-test="login-button"]'); + }); + + await test.step('Verify error message is displayed', async () => { + const error = page.locator('[data-test="error"]'); + await expect(error).toBeVisible(); + await expect(error).toContainText('Username and password do not match'); + }); + + qase.comment('Error correctly displayed — tracked in both projects'); }); - // 2) Via qase.projectsTitle() in the test name (same as single-project qase(id, name)) - test(qase.projectsTitle('Multi-project via title', { PROJ1: [3,8], PROJ2: [4,7] }), async () => { - expect(2 + 2).toBe(4); + // Alternative: qase.projectsTitle() in the test name + test(qase.projectsTitle('Locked user cannot login', { PROJ1: [5], PROJ2: [6] }), async ({ page }) => { + qase.fields({ severity: 'high', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'locked_out_user' }); + + await test.step('Attempt login with locked user', async () => { + await page.fill('[data-test="username"]', 'locked_out_user'); + await page.fill('[data-test="password"]', 'secret_sauce'); + await page.click('[data-test="login-button"]'); + }); + + await test.step('Verify locked-out error message', async () => { + const error = page.locator('[data-test="error"]'); + await expect(error).toBeVisible(); + await expect(error).toContainText('Sorry, this user has been locked out'); + }); + }); +}); + +test.describe('Multi-project Cart Scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('https://www.saucedemo.com'); + await page.fill('[data-test="username"]', 'standard_user'); + await page.fill('[data-test="password"]', 'secret_sauce'); + await page.click('[data-test="login-button"]'); + await expect(page).toHaveURL(/.*inventory.html/); }); - // 3) Via annotation: type "QaseProjects", description JSON + // Report to PROJ1 (cases 7, 8) and PROJ2 (case 9) + test('Add product to cart', async ({ page }) => { + qase.projects({ PROJ1: [7, 8], PROJ2: [9] }); + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCart\tAdd Items'); + qase.parameters({ product: 'Sauce Labs Backpack' }); + + await test.step('Add Sauce Labs Backpack to cart', async () => { + await page.click('[data-test="add-to-cart-sauce-labs-backpack"]'); + }); + + await test.step('Verify cart badge', async () => { + await expect(page.locator('.shopping_cart_badge')).toHaveText('1'); + }); + + await test.step('Navigate to cart and verify', async () => { + await page.click('.shopping_cart_link'); + await expect(page.locator('.inventory_item_name')).toContainText('Backpack'); + }); + + qase.comment('Product added to cart — reported to both projects'); + + const screenshot = await page.screenshot({ encoding: 'base64' }); + qase.attach({ name: 'cart-page.png', content: screenshot, contentType: 'image/png' }); + }); + + // Report to PROJ1 (case 10) and PROJ2 (case 11) + test('Complete checkout flow', async ({ page }) => { + qase.projects({ PROJ1: [10], PROJ2: [11] }); + qase.fields({ severity: 'critical', priority: 'critical', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + await test.step('Add product and go to checkout', async () => { + await page.click('[data-test="add-to-cart-sauce-labs-backpack"]'); + await page.click('.shopping_cart_link'); + await page.click('[data-test="checkout"]'); + }); + + await test.step('Fill checkout information', async () => { + await page.fill('[data-test="firstName"]', 'John'); + await page.fill('[data-test="lastName"]', 'Doe'); + await page.fill('[data-test="postalCode"]', '12345'); + }); + + await test.step('Complete and verify order', async () => { + await page.click('[data-test="continue"]'); + await page.click('[data-test="finish"]'); + await expect(page.locator('.complete-header')).toContainText('Thank you'); + }); + + qase.comment('Full checkout flow verified across both projects'); + + qase.attach({ + name: 'order-details.json', + content: JSON.stringify({ + customer: { firstName: 'John', lastName: 'Doe', postalCode: '12345' }, + product: 'Sauce Labs Backpack', + status: 'completed' + }, null, 2), + contentType: 'application/json' + }); + }); + + // Alternative: annotation-based approach test( 'Multi-project via annotation', - { annotation: { type: 'QaseProjects', description: '{"PROJ1":[5],"PROJ2":[6]}' } }, - async () => { - expect(true).toBe(true); + { annotation: { type: 'QaseProjects', description: '{"PROJ1":[12],"PROJ2":[13]}' } }, + async ({ page }) => { + qase.suite('E-commerce\tInventory\tBrowse'); + + await test.step('Verify inventory page loaded', async () => { + await expect(page.locator('[data-test="title"]')).toHaveText('Products'); + }); + + await test.step('Verify products are displayed', async () => { + const items = page.locator('.inventory_item'); + await expect(items).toHaveCount(6); + }); + + qase.comment('Inventory page verified via annotation-based multi-project mapping'); }, ); }); diff --git a/examples/multiProject/testcafe/multiProjectTests.js b/examples/multiProject/testcafe/multiProjectTests.js index c1fe7ce1..745fb59c 100644 --- a/examples/multiProject/testcafe/multiProjectTests.js +++ b/examples/multiProject/testcafe/multiProjectTests.js @@ -1,13 +1,177 @@ import { test } from 'testcafe'; import { qase } from 'testcafe-reporter-qase/qase'; -fixture`Multi-project example`.page`http://devexpress.github.io/testcafe/example/`; +fixture`Multi-project Login Scenarios` + .page`https://www.saucedemo.com`; -// Map to case 1 in PROJ1 and case 2 in PROJ2. Replace IDs with real case IDs in your projects. -test.meta(qase.projects({ PROJ1: [1], PROJ2: [2] }).create())('A test reported to two projects', async (t) => { - await t.expect(true).ok(); +// Report login test to both PROJ1 (case 1) and PROJ2 (case 2) +test.meta(qase.projects({ PROJ1: [1], PROJ2: [2] }).title('User can login with valid credentials').fields({ + severity: 'critical', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tAuthentication\tLogin').create())('Valid login', async t => { + await qase.step('Navigate to login page', async () => { + const loginButton = await t.eval(() => !!document.querySelector('[data-test="login-button"]')); + await t.expect(loginButton).ok('Login button should be visible'); + }); + + await qase.step('Enter valid credentials', async () => { + await t + .typeText('[data-test="username"]', 'standard_user') + .typeText('[data-test="password"]', 'secret_sauce'); + }); + + await qase.step('Submit login form', async () => { + await t.click('[data-test="login-button"]'); + }); + + await qase.step('Verify successful login', async () => { + const title = await t.eval(() => document.querySelector('[data-test="title"]')?.textContent); + await t.expect(title).eql('Products', 'Should redirect to inventory page'); + }); + + await qase.comment('Login verified across both projects with standard credentials'); + await qase.attach({ + name: 'login-credentials.txt', + content: 'Username: standard_user\nPassword: secret_sauce', + type: 'text/plain' + }); +}); + +// Report invalid login to PROJ1 (case 3) and PROJ2 (case 4) +test.meta(qase.projects({ PROJ1: [3], PROJ2: [4] }).title('Invalid password shows error').fields({ + severity: 'high', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tAuthentication\tLogin').parameters({ + username: 'standard_user', + password: 'wrong_password' +}).create())('Invalid password login', async t => { + await qase.step('Enter invalid credentials', async () => { + await t + .typeText('[data-test="username"]', 'standard_user') + .typeText('[data-test="password"]', 'wrong_password'); + }); + + await qase.step('Submit login form', async () => { + await t.click('[data-test="login-button"]'); + }); + + await qase.step('Verify error message', async () => { + const errorContainer = await t.eval(() => !!document.querySelector('[data-test="error"]')); + await t.expect(errorContainer).ok('Error message should be displayed'); + }); + + await qase.comment('Invalid credentials correctly rejected in both projects'); +}); + +fixture`Multi-project Cart Scenarios` + .page`https://www.saucedemo.com` + .beforeEach(async t => { + await t + .typeText('[data-test="username"]', 'standard_user') + .typeText('[data-test="password"]', 'secret_sauce') + .click('[data-test="login-button"]'); + }); + +// Report cart test to PROJ1 (cases 5, 6) and PROJ2 (case 7) +test.meta(qase.projects({ PROJ1: [5, 6], PROJ2: [7] }).title('Add product to cart').fields({ + severity: 'critical', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tCart\tAdd Items').parameters({ + product: 'Sauce Labs Backpack' +}).create())('Add to cart', async t => { + await qase.step('Add Sauce Labs Backpack to cart', async () => { + await t.click('[data-test="add-to-cart-sauce-labs-backpack"]'); + }); + + await qase.step('Verify cart badge shows 1', async () => { + const badge = await t.eval(() => document.querySelector('.shopping_cart_badge')?.textContent); + await t.expect(badge).eql('1', 'Cart badge should show 1 item'); + }); + + await qase.step('Navigate to cart', async () => { + await t.click('.shopping_cart_link'); + }); + + await qase.step('Verify product in cart', async () => { + const itemName = await t.eval(() => document.querySelector('.inventory_item_name')?.textContent); + await t.expect(itemName).eql('Sauce Labs Backpack', 'Correct product in cart'); + }); + + await qase.comment('Product added to cart and reported to both projects'); + + await qase.attach({ + name: 'cart-state.json', + content: JSON.stringify({ + product: 'Sauce Labs Backpack', + quantity: 1, + projects: ['PROJ1', 'PROJ2'] + }, null, 2), + type: 'application/json' + }); +}); + +// Report checkout test to PROJ1 (case 8) and PROJ2 (case 9) +test.meta(qase.projects({ PROJ1: [8], PROJ2: [9] }).title('Complete checkout flow').fields({ + severity: 'critical', + priority: 'critical', + layer: 'e2e' +}).suite('E-commerce\tCheckout\tComplete').parameters({ + firstName: 'John', + lastName: 'Doe', + postalCode: '12345' +}).create())('Complete checkout', async t => { + await qase.step('Add product and go to cart', async () => { + await t + .click('[data-test="add-to-cart-sauce-labs-backpack"]') + .click('.shopping_cart_link'); + }); + + await qase.step('Start checkout', async () => { + await t.click('[data-test="checkout"]'); + }); + + await qase.step('Fill checkout information', async (s1) => { + await s1.step('Enter first name', async () => { + await t.typeText('[data-test="firstName"]', 'John'); + }); + + await s1.step('Enter last name', async () => { + await t.typeText('[data-test="lastName"]', 'Doe'); + }); + + await s1.step('Enter postal code', async () => { + await t.typeText('[data-test="postalCode"]', '12345'); + }); + }); + + await qase.step('Continue and finish', async () => { + await t + .click('[data-test="continue"]') + .click('[data-test="finish"]'); + }); + + await qase.step('Verify order confirmation', async () => { + const header = await t.eval(() => document.querySelector('.complete-header')?.textContent); + await t.expect(header).eql('Thank you for your order!', 'Should show confirmation message'); + }); + + await qase.comment('Full checkout flow verified across both projects'); + + await qase.attach({ + name: 'order-details.json', + content: JSON.stringify({ + customer: { firstName: 'John', lastName: 'Doe', postalCode: '12345' }, + product: 'Sauce Labs Backpack', + status: 'completed' + }, null, 2), + type: 'application/json' + }); }); -test.meta(qase.projects({ PROJ1: [10, 11], PROJ2: [20] }).create())('Another test with multiple cases per project', async (t) => { - await t.expect(1 + 1).eql(2); +test.meta(qase.projects({ PROJ1: [10], PROJ2: [11] }).ignore().create())('Ignored multi-project test', async t => { + await t.expect(true).ok('This will not be reported to Qase'); + await qase.comment('This test demonstrates ignore with multi-project'); }); diff --git a/examples/multiProject/vitest/test/multiProject.test.ts b/examples/multiProject/vitest/test/multiProject.test.ts index 45ecc782..e949d86d 100644 --- a/examples/multiProject/vitest/test/multiProject.test.ts +++ b/examples/multiProject/vitest/test/multiProject.test.ts @@ -1,22 +1,123 @@ import { describe, test, expect } from 'vitest'; -import { addQaseProjects } from 'vitest-qase-reporter/vitest'; +import { addQaseProjects, withQase } from 'vitest-qase-reporter/vitest'; -describe('Multi-project example', () => { - // Map this test to case 1 in PROJ1 and case 2 in PROJ2. Replace IDs with real case IDs in your projects. +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +describe('Multi-project API - User Operations', () => { + // Report to PROJ1 (case 1) and PROJ2 (case 2) + test( + addQaseProjects('GET all users returns 10 users', { PROJ1: [1], PROJ2: [2] }), + withQase(async ({ qase }) => { + await qase.fields({ layer: 'api', severity: 'normal', priority: 'high' }); + + await qase.step('Send GET request to /users endpoint', async () => { + const response = await fetch(`${BASE_URL}/users`); + expect(response.status).toBe(200); + + const users = await response.json(); + expect(users).toHaveLength(10); + }); + + await qase.step('Verify user data structure', async () => { + const response = await fetch(`${BASE_URL}/users`); + const users = await response.json(); + + expect(users[0]).toHaveProperty('id'); + expect(users[0]).toHaveProperty('name'); + expect(users[0]).toHaveProperty('email'); + }); + + await qase.comment('All users returned — reported to PROJ1 and PROJ2'); + }), + ); + + // Report to PROJ1 (case 3) and PROJ2 (case 4) + test( + addQaseProjects('GET single user by ID returns correct user', { PROJ1: [3], PROJ2: [4] }), + withQase(async ({ qase }) => { + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.parameters({ userId: 1 }); + + await qase.step('Send GET request to /users/1', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user.id).toBe(1); + expect(user.name).toBe('Leanne Graham'); + expect(user.email).toBe('Sincere@april.biz'); + }); + + await qase.step('Verify user has address and company', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + const user = await response.json(); + + expect(user.address).toHaveProperty('city'); + expect(user.company).toHaveProperty('name'); + }); + + await qase.comment('User details verified — tracked across both projects'); + }), + ); + + // Report to PROJ1 (case 5) and PROJ2 (case 6) test( - addQaseProjects('A test reported to two projects', { PROJ1: [1], PROJ2: [2] }), - () => { - expect(true).toBe(true); - }, + addQaseProjects('POST create new user returns 201 with ID', { PROJ1: [5], PROJ2: [6] }), + withQase(async ({ qase }) => { + await qase.fields({ layer: 'api', severity: 'critical', priority: 'high' }); + + const newUser = { + name: 'Test User', + username: 'testuser', + email: 'test@example.com', + }; + + await qase.step('Send POST request with new user data', async () => { + const response = await fetch(`${BASE_URL}/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newUser), + }); + + expect(response.status).toBe(201); + + const createdUser = await response.json(); + expect(createdUser).toHaveProperty('id'); + }); + + await qase.step('Attach request payload', async () => { + await qase.attach({ + name: 'request-body.json', + content: JSON.stringify(newUser, null, 2), + type: 'application/json', + }); + }); + + await qase.comment('User created — reported to PROJ1 and PROJ2'); + }), ); + // Report to PROJ1 (case 7) and PROJ2 (case 8) test( - addQaseProjects('Another test with multiple cases per project', { - PROJ1: [10, 11], - PROJ2: [20], + addQaseProjects('DELETE user returns 200 status', { PROJ1: [7], PROJ2: [8] }), + withQase(async ({ qase }) => { + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.comment('Note: JSONPlaceholder fakes DELETE — no actual deletion occurs'); + + await qase.step('Send DELETE request to /users/1', async () => { + const response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + expect(response.status).toBe(200); + }); + + await qase.step('Verify response is empty object', async () => { + const response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + const result = await response.json(); + expect(result).toEqual({}); + }); }), - () => { - expect(1 + 1).toBe(2); - }, ); }); diff --git a/examples/multiProject/wdio/test/multiProject.spec.js b/examples/multiProject/wdio/test/multiProject.spec.js index c0192609..53010e83 100644 --- a/examples/multiProject/wdio/test/multiProject.spec.js +++ b/examples/multiProject/wdio/test/multiProject.spec.js @@ -1,18 +1,158 @@ const { qase } = require('wdio-qase-reporter'); -describe('Multi-project example', () => { - // Map this test to case 1 in PROJ1 and case 2 in PROJ2. Replace IDs with real case IDs in your projects. - it(qase.projects({ PROJ1: [1], PROJ2: [2] }, 'A test reported to two projects'), () => { - expect(true).to.equal(true); +describe('Multi-project Login Scenarios', () => { + // Report login test to PROJ1 (case 1) and PROJ2 (case 2) + it(qase.projects({ PROJ1: [1], PROJ2: [2] }, 'User can login with valid credentials'), async () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + + await qase.step('Open login page', async () => { + await browser.url('https://www.saucedemo.com'); + await expect($('[data-test="username"]')).toBeDisplayed(); + }); + + await qase.step('Enter valid credentials and submit', async () => { + await $('[data-test="username"]').setValue('standard_user'); + await $('[data-test="password"]').setValue('secret_sauce'); + await $('[data-test="login-button"]').click(); + }); + + await qase.step('Verify successful login', async () => { + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/inventory.html'), + { timeout: 5000, timeoutMsg: 'Expected to navigate to inventory page' } + ); + await expect(browser).toHaveUrl('https://www.saucedemo.com/inventory.html'); + }); + + qase.comment('Login successful — reported to both PROJ1 and PROJ2'); + qase.attach({ + name: 'login-credentials.txt', + content: 'Username: standard_user\nPassword: secret_sauce', + type: 'text/plain' + }); + }); + + // Report invalid login to PROJ1 (case 3) and PROJ2 (case 4) + it(qase.projects({ PROJ1: [3], PROJ2: [4] }, 'Invalid password shows error'), async () => { + qase.fields({ severity: 'high', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'standard_user', password: 'wrong_password' }); + + await qase.step('Open login page', async () => { + await browser.url('https://www.saucedemo.com'); + }); + + await qase.step('Enter invalid credentials', async () => { + await $('[data-test="username"]').setValue('standard_user'); + await $('[data-test="password"]').setValue('wrong_password'); + await $('[data-test="login-button"]').click(); + }); + + await qase.step('Verify error message is displayed', async () => { + await expect($('[data-test="error"]')).toBeDisplayed(); + await expect($('[data-test="error"]')).toHaveTextContaining('Username and password do not match'); + }); + + qase.comment('Error message correctly displayed — tracked in both projects'); + }); +}); + +describe('Multi-project Cart Scenarios', () => { + beforeEach(async () => { + await browser.url('https://www.saucedemo.com'); + await $('[data-test="username"]').setValue('standard_user'); + await $('[data-test="password"]').setValue('secret_sauce'); + await $('[data-test="login-button"]').click(); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/inventory.html'), + { timeout: 5000 } + ); }); - it( - qase.projects( - { PROJ1: [10, 11], PROJ2: [20] }, - 'Another test with multiple cases per project', - ), - () => { - expect(1 + 1).to.equal(2); - }, - ); + // Report cart test to PROJ1 (cases 5, 6) and PROJ2 (case 7) + it(qase.projects({ PROJ1: [5, 6], PROJ2: [7] }, 'User can add product to cart'), async () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCart\tAdd Items'); + qase.parameters({ product: 'Sauce Labs Backpack' }); + + await qase.step('Add product to cart', async () => { + await $('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + }); + + await qase.step('Verify cart badge shows item count', async () => { + await expect($('.shopping_cart_badge')).toHaveText('1'); + }); + + await qase.step('Navigate to cart page', async () => { + await $('.shopping_cart_link').click(); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/cart.html'), + { timeout: 5000 } + ); + }); + + await qase.step('Verify product is in cart', async () => { + await expect($('.cart_item .inventory_item_name')).toHaveTextContaining('Backpack'); + }); + + qase.comment('Product added to cart — reported to both projects'); + + qase.attach({ + name: 'cart-state.json', + content: JSON.stringify({ + product: 'Sauce Labs Backpack', + quantity: 1, + projects: ['PROJ1', 'PROJ2'] + }, null, 2), + type: 'application/json' + }); + }); + + // Report checkout to PROJ1 (case 8) and PROJ2 (case 9) + it(qase.projects({ PROJ1: [8], PROJ2: [9] }, 'User can complete checkout'), async () => { + qase.fields({ severity: 'critical', priority: 'critical', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete Purchase'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + await qase.step('Add product and go to cart', async () => { + await $('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + await $('.shopping_cart_link').click(); + }); + + await qase.step('Start checkout', async () => { + await $('[data-test="checkout"]').click(); + }); + + await qase.step('Fill checkout information', async (step) => { + await step.step('Enter first name', async () => { + await $('[data-test="firstName"]').setValue('John'); + }); + + await step.step('Enter last name', async () => { + await $('[data-test="lastName"]').setValue('Doe'); + }); + + await step.step('Enter postal code', async () => { + await $('[data-test="postalCode"]').setValue('12345'); + }); + }); + + await qase.step('Continue and finish', async () => { + await $('[data-test="continue"]').click(); + await $('[data-test="finish"]').click(); + }); + + await qase.step('Verify order completion', async () => { + await expect($('.complete-header')).toHaveTextContaining('Thank you'); + }); + + qase.attach({ + name: 'order-complete.txt', + content: 'Order completed for John Doe — reported to PROJ1 and PROJ2', + type: 'text/plain' + }); + + qase.comment('Checkout completed — tracked across both projects'); + }); }); diff --git a/examples/single/cucumberjs/README.md b/examples/single/cucumberjs/README.md index 8f832975..839ee0db 100644 --- a/examples/single/cucumberjs/README.md +++ b/examples/single/cucumberjs/README.md @@ -1,81 +1,154 @@ -# CucumberJS Example +# CucumberJS BDD Example -This is a sample project demonstrating how to write and execute tests using the CucumberJS framework with integration to Qase Test Management. +## Overview -## Prerequisites +This example demonstrates realistic BDD (Behavior-Driven Development) API testing using CucumberJS with Qase Test Management integration. Tests exercise the JSONPlaceholder REST API with Gherkin feature files expressing business behavior. All Qase metadata is configured using Gherkin tags only, with no programmatic imports required. -Ensure that the following tools are installed on your machine: +## Prerequisites -1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) -2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) +1. [Node.js](https://nodejs.org/) (version 18 or higher required for native fetch) +2. [npm](https://www.npmjs.com/) -## Setup Instructions +## Installation -1. Clone this repository by running the following commands: +1. Clone the repository: ```bash git clone https://github.com/qase-tms/qase-javascript.git cd qase-javascript/examples/single/cucumberjs ``` -2. Install the project dependencies: +2. Install dependencies: ```bash npm install ``` -3. Create a `qase.config.json` file in the root of the project. Follow the instructions on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). - -## Example Files - -This example includes: - -* **features/** — Gherkin feature files with test scenarios - * `simple.feature` — Basic scenarios with Qase tags (@QaseID, @QaseTitle, @QaseFields) - * `table.feature` — Examples using data tables -* **step_definitions/** — Step implementation files - * `simple_steps.js` — Step definitions using native Cucumber Given/When/Then - * `table_steps.js` — Step definitions for table-based scenarios -* **qase.config.json** — Qase reporter configuration - -## Running Tests - -To run tests locally without reporting to Qase: - -```bash -QASE_MODE=off npm test +3. Configure Qase credentials in `qase.config.json`: + - Set your API token in `testops.api.token` + - Set your project code in `testops.project` + +## Configuration + +The Qase reporter can be configured using environment variables or configuration files. + +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) + +Example `qase.config.json`: + +```json +{ + "debug": true, + "testops": { + "api": { + "token": "your_api_token_here" + }, + "project": "YOUR_PROJECT_CODE", + "uploadAttachments": true, + "run": { + "complete": true, + "title": "CucumberJS BDD Test Run" + } + } +} ``` -To run tests and upload the results to Qase Test Management: +## Running Tests ```bash +# Run tests without Qase reporting (default) npm test -``` -Or with explicit mode: - -```bash -QASE_MODE=testops npx cucumber-js -f cucumberjs-qase-reporter +# Run tests with Qase reporting +QASE_MODE=testops npm test ``` -## Expected Behavior +## Test Scenarios + +### features/api-crud.feature (4 scenarios) +User CRUD operations against JSONPlaceholder: +- **Get all users** -- Verify 10 users returned with required fields +- **Get single user by ID** -- Verify specific user data (Leanne Graham) +- **Create new user** -- POST with JSON body, verify 201 response +- **Delete user** -- DELETE request, verify 200 response + +### features/api-posts.feature (3 scenarios + Scenario Outline) +Post validation and filtering: +- **Get all posts** -- Verify 100 posts returned +- **Filter posts by user ID** -- Scenario Outline with 3 user IDs (parameterized) +- **Get post with comments** -- Verify nested resource returns 5 comments + +### features/api-errors.feature (4 scenarios) +Error handling behavior: +- **Non-existent user** -- Verify 200 response with empty object (JSONPlaceholder behavior) +- **Non-existent post** -- Verify 200 response with empty object +- **Invalid endpoint** -- Verify 404 for unknown routes +- **POST with empty body** -- Verify graceful handling (201 with ID) + +### features/api-advanced.feature (4 scenarios) +Advanced Qase integration patterns: +- **Fetch user and their posts** -- Multi-step relationship test with @QaseParameters +- **Suite hierarchy and group parameters** -- @QaseGroupParameters demonstration +- **Parameters tag with Scenario Outline** -- @QaseParameters overriding Examples params +- **Ignored test** -- @QaseIgnore excluding from Qase reporting + +## Qase Features Demonstrated + +| Feature | How It's Used | Example | +|---------|---------------|---------| +| Test Case ID | `@QaseID=N` tag on scenarios | `@QaseID=1` | +| Title Override | `@QaseTitle=Name` tag (underscores for spaces) | `@QaseTitle=Get_all_users_returns_10_users` | +| Custom Fields | `@QaseFields=JSON` tag (compact, no spaces) | `@QaseFields={"severity":"critical","layer":"api"}` | +| Suite Hierarchy | `@QaseSuite=Path` tag (tab-separated levels) | `@QaseSuite=API\tUsers\tRead` | +| Parameters | `@QaseParameters=JSON` tag | `@QaseParameters={"testScope":"user_posts_relationship"}` | +| Group Parameters | `@QaseGroupParameters=JSON` tag | `@QaseGroupParameters={"environment":"production"}` | +| Ignore | `@QaseIgnore` tag | Excludes scenario from Qase reporting | +| Steps | Native Gherkin Given/When/Then | Auto-mapped to Qase steps | +| Attachments | `this.attach(content, mimeType)` in steps | JSON response data attached to results | +| Parameterization | Scenario Outline with Examples table | Parameters auto-extracted from Examples | + +## CucumberJS-Specific Patterns + +- **Tag-based metadata** -- All Qase configuration uses Gherkin tags, not programmatic imports +- **No `qase` import needed** -- Unlike other frameworks, there is no `qase` object to import +- **Native step mapping** -- Given/When/Then steps are automatically reported as Qase test steps +- **`this.attach()` for attachments** -- Use Cucumber's native attachment API, not `qase.attach()` +- **`function()` not `=>` in steps** -- Arrow functions break Cucumber's World context +- **No spaces in tags** -- Use underscores for titles, compact JSON without spaces for fields +- **Profile-based config** -- `cucumber.js` file configures formatter and step definition paths + +## Project Structure -When tests execute with Qase reporting enabled: - -* **Gherkin scenarios** are reported as individual test cases in Qase -* **Given/When/Then/And steps** from feature files are automatically reported as Qase test steps -* **@QaseID tags** link scenarios to existing test cases in your Qase project -* **@QaseTitle tags** override the default scenario name in Qase -* **@QaseFields tags** add metadata (severity, priority, etc.) to test results -* **Attachments** added via `this.attach()` in step definitions are included in Qase results - -## Framework-Specific Features +``` +cucumberjs/ +├── features/ +│ ├── api-crud.feature # User CRUD operations +│ ├── api-posts.feature # Post validation and filtering +│ ├── api-errors.feature # Error handling scenarios +│ └── api-advanced.feature # Advanced Qase features +├── step_definitions/ +│ ├── api-crud.steps.js # CRUD step implementations +│ ├── api-posts.steps.js # Post step implementations +│ ├── api-errors.steps.js # Error step implementations +│ └── api-advanced.steps.js # Advanced step implementations +├── cucumber.js # Cucumber configuration +├── qase.config.json # Qase reporter configuration +└── package.json +``` -CucumberJS with Qase has unique patterns: +## API Notes -* **No programmatic qase.step() API** — Steps come from Gherkin syntax (Given/When/Then) -* **Native Cucumber attachments** — Use `this.attach(content, mimeType)` in step definitions (not `qase.attach()`) -* **Tag-based metadata** — Test configuration uses Gherkin tags instead of programmatic calls -* **Feature-based organization** — Test suite hierarchy comes from Feature/Scenario structure +Tests use [JSONPlaceholder](https://jsonplaceholder.typicode.com/) as the test API: +- Free, public REST API -- no authentication required +- Returns realistic data (users, posts, comments, todos) +- Write operations (POST, PUT, DELETE) are faked -- they return success responses but don't persist data +- Non-existent resources (e.g., `/users/999`) return status 200 with an empty object `{}` +- Only truly invalid endpoints (e.g., `/invalid-endpoint`) return 404 +- Stable and widely used for testing and prototyping ## Additional Resources -For more details on how to use this integration with Qase Test Management, visit the [Qase CucumberJS documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-cucumberjs). +- [Qase CucumberJS Reporter](https://github.com/qase-tms/qase-javascript/tree/main/qase-cucumberjs) +- [CucumberJS Documentation](https://cucumber.io/docs/installation/javascript/) +- [JSONPlaceholder API Guide](https://jsonplaceholder.typicode.com/guide/) diff --git a/examples/single/cucumberjs/cucumber.js b/examples/single/cucumberjs/cucumber.js new file mode 100644 index 00000000..3c3c1b4e --- /dev/null +++ b/examples/single/cucumberjs/cucumber.js @@ -0,0 +1,7 @@ +module.exports = { + default: { + format: ['progress', 'cucumberjs-qase-reporter'], + require: ['step_definitions/**/*.js'], + publishQuiet: true, + }, +}; diff --git a/examples/single/cucumberjs/features/api-advanced.feature b/examples/single/cucumberjs/features/api-advanced.feature new file mode 100644 index 00000000..de2a38d0 --- /dev/null +++ b/examples/single/cucumberjs/features/api-advanced.feature @@ -0,0 +1,47 @@ +Feature: Advanced Qase Features + Demonstrates advanced CucumberJS-Qase integration patterns + including parameters, suite hierarchy, group parameters, and ignore + + Background: + Given the API is available at "https://jsonplaceholder.typicode.com" + + @QaseID=12 + @QaseTitle=Fetch_user_and_their_posts_relationship + @QaseFields={"severity":"normal","priority":"medium","layer":"api"} + @QaseSuite=API\tAdvanced\tRelationships + @QaseParameters={"testScope":"user_posts_relationship"} + Scenario: Fetch user and their posts + When I send a GET request to "/users/1" + Then the response status should be 200 + And the response "name" should be "Leanne Graham" + When I send a GET request to "/posts?userId=1" + Then the response status should be 200 + And the response should contain 10 items + + @QaseID=13 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tAdvanced\tData_Validation + @QaseGroupParameters={"environment":"production","region":"us-east"} + Scenario: Suite hierarchy and group parameters demonstration + When I send a GET request to "/todos/1" + Then the response status should be 200 + And the response should have a "completed" field + + @QaseID=14 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tAdvanced\tParameterized + @QaseParameters={"testType":"override_demo"} + Scenario Outline: Parameters tag with Scenario Outline + When I send a GET request to "/comments?postId=" + Then the response status should be 200 + And the response should contain 5 items + + Examples: + | postId | + | 1 | + | 2 | + + @QaseIgnore + Scenario: Ignored test - not reported to Qase + When I send a GET request to "/users/1" + Then the response status should be 200 diff --git a/examples/single/cucumberjs/features/api-crud.feature b/examples/single/cucumberjs/features/api-crud.feature new file mode 100644 index 00000000..ca2013c8 --- /dev/null +++ b/examples/single/cucumberjs/features/api-crud.feature @@ -0,0 +1,49 @@ +Feature: User CRUD Operations + As an API consumer + I want to manage users via REST API + So that I can verify CRUD operations work correctly + + Background: + Given the API is available at "https://jsonplaceholder.typicode.com" + + @QaseID=1 + @QaseTitle=Get_all_users_returns_10_users + @QaseFields={"severity":"normal","priority":"high","layer":"api"} + @QaseSuite=API\tUsers\tRead + Scenario: Get all users + When I send a GET request to "/users" + Then the response status should be 200 + And the response should contain 10 items + And each item should have an "id" field + And each item should have an "email" field + + @QaseID=2 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tUsers\tRead + Scenario: Get single user by ID + When I send a GET request to "/users/1" + Then the response status should be 200 + And the response "name" should be "Leanne Graham" + And the response "email" should be "Sincere@april.biz" + + @QaseID=3 + @QaseFields={"severity":"critical","priority":"high","layer":"api"} + @QaseSuite=API\tUsers\tCreate + Scenario: Create new user + When I send a POST request to "/users" with body: + """ + { + "name": "Test User", + "username": "testuser", + "email": "test@example.com" + } + """ + Then the response status should be 201 + And the response should have an "id" field + + @QaseID=4 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tUsers\tDelete + Scenario: Delete user + When I send a DELETE request to "/users/1" + Then the response status should be 200 diff --git a/examples/single/cucumberjs/features/api-errors.feature b/examples/single/cucumberjs/features/api-errors.feature new file mode 100644 index 00000000..f791b0d0 --- /dev/null +++ b/examples/single/cucumberjs/features/api-errors.feature @@ -0,0 +1,41 @@ +Feature: Error Handling + As an API consumer + I want the API to handle errors gracefully + So that error responses are predictable + + Background: + Given the API is available at "https://jsonplaceholder.typicode.com" + + @QaseID=8 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tErrors\tNot_Found + Scenario: Non-existent user returns empty object + When I send a GET request to "/users/999" + Then the response status should be 200 + And the response should be an empty object + + @QaseID=9 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tErrors\tNot_Found + Scenario: Non-existent post returns empty object + When I send a GET request to "/posts/999" + Then the response status should be 200 + And the response should be an empty object + + @QaseID=10 + @QaseFields={"severity":"low","layer":"api"} + @QaseSuite=API\tErrors\tInvalid_Endpoint + Scenario: Invalid endpoint returns 404 + When I send a GET request to "/invalid-endpoint" + Then the response status should be 404 + + @QaseID=11 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tErrors\tValidation + Scenario: POST with empty body is handled gracefully + When I send a POST request to "/posts" with body: + """ + {} + """ + Then the response status should be 201 + And the response should have an "id" field diff --git a/examples/single/cucumberjs/features/api-posts.feature b/examples/single/cucumberjs/features/api-posts.feature new file mode 100644 index 00000000..3eabce13 --- /dev/null +++ b/examples/single/cucumberjs/features/api-posts.feature @@ -0,0 +1,38 @@ +Feature: Post Validation + As an API consumer + I want to filter and validate posts + So that post data integrity is verified + + Background: + Given the API is available at "https://jsonplaceholder.typicode.com" + + @QaseID=5 + @QaseFields={"severity":"normal","priority":"high","layer":"api"} + @QaseSuite=API\tPosts\tRead + Scenario: Get all posts + When I send a GET request to "/posts" + Then the response status should be 200 + And the response should contain 100 items + + @QaseID=6 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tPosts\tFiltering + Scenario Outline: Filter posts by user ID + When I send a GET request to "/posts?userId=" + Then the response status should be 200 + And all items should have "userId" equal to + + Examples: + | userId | + | 1 | + | 2 | + | 3 | + + @QaseID=7 + @QaseFields={"severity":"normal","layer":"api"} + @QaseSuite=API\tPosts\tRead + Scenario: Get post with comments + When I send a GET request to "/posts/1/comments" + Then the response status should be 200 + And the response should contain 5 items + And each item should have an "email" field diff --git a/examples/single/cucumberjs/features/simple.feature b/examples/single/cucumberjs/features/simple.feature deleted file mode 100644 index 72cd8964..00000000 --- a/examples/single/cucumberjs/features/simple.feature +++ /dev/null @@ -1,41 +0,0 @@ -Feature: Simple feature - It is a simple feature with simple scenarios - - Scenario: Scenario without steps - - Scenario: Scenario with one step - Given I have a step - - Scenario: Scenario with multiple steps - Given I have a step - And I have another step - When I do something - Then I expect something to happen - - @Q-1 - Scenario: Scenario with old Qase ID tag - Given I have a step - - @QaseID=2 - Scenario: Scenario with new Qase ID tag - Given I have a step - - @QaseTitle=Scenario_with_Qase_title_tag - Scenario: Scenario with Qase title tag - Given I have a step - - @QaseFields={"description":"Description","severity":"major"} - Scenario: Scenario with Qase fields tag - Given I have a step - - Scenario: Scenario with filed last step - Given I have a step - And I have another step - When I do something - Then I fail - - Scenario: Scenario with filed step - Given I have a step - And I have another step - When I fail - Then I expect something to happen diff --git a/examples/single/cucumberjs/features/table.feature b/examples/single/cucumberjs/features/table.feature deleted file mode 100644 index 17eca1a1..00000000 --- a/examples/single/cucumberjs/features/table.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: Table feature - It is a table feature - - Scenario Outline: Table scenario - Given I have a table with rows - Then the table should have rows - - Examples: - | rows | - | 1 | - | 2 | - | 3 | diff --git a/examples/single/cucumberjs/package.json b/examples/single/cucumberjs/package.json index ee851eb1..db772853 100644 --- a/examples/single/cucumberjs/package.json +++ b/examples/single/cucumberjs/package.json @@ -2,10 +2,10 @@ "name": "examples-cucumberjs", "private": true, "scripts": { - "test": "QASE_MODE=testops cucumber-js -f cucumberjs-qase-reporter features -r step_definitions --publish-quiet" + "test": "QASE_MODE=${QASE_MODE:-off} cucumber-js" }, "devDependencies": { - "@cucumber/cucumber": "^7.3.2", - "cucumberjs-qase-reporter": "^2.1.6" + "@cucumber/cucumber": "^11.0.0", + "cucumberjs-qase-reporter": "^2.2.0" } } diff --git a/examples/single/cucumberjs/qase.config.json b/examples/single/cucumberjs/qase.config.json index d025c79f..b255dc2d 100644 --- a/examples/single/cucumberjs/qase.config.json +++ b/examples/single/cucumberjs/qase.config.json @@ -1,13 +1,11 @@ { "debug": true, - "testops": { "api": { "token": "api_key" }, "project": "project_code", "uploadAttachments": true, - "showPublicReportLink": true, "run": { "complete": true } diff --git a/examples/single/cucumberjs/step_definitions/api_steps.js b/examples/single/cucumberjs/step_definitions/api_steps.js new file mode 100644 index 00000000..c3823cbe --- /dev/null +++ b/examples/single/cucumberjs/step_definitions/api_steps.js @@ -0,0 +1,100 @@ +const { Given, When, Then } = require('@cucumber/cucumber'); +const assert = require('assert'); + +Given('the API is available at {string}', function(baseUrl) { + this.baseUrl = baseUrl; +}); + +When('I send a GET request to {string}', async function(endpoint) { + const url = `${this.baseUrl}${endpoint}`; + this.response = await fetch(url); + this.responseData = await this.response.json(); + + // Attach response data as JSON (demonstrates this.attach()) + this.attach(JSON.stringify({ + url: url, + status: this.response.status, + body: Array.isArray(this.responseData) + ? `[${this.responseData.length} items]` + : this.responseData, + }, null, 2), 'application/json'); +}); + +When('I send a POST request to {string} with body:', async function(endpoint, docString) { + const url = `${this.baseUrl}${endpoint}`; + this.response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: docString, + }); + this.responseData = await this.response.json(); + + // Attach request and response + this.attach(JSON.stringify({ + request: { url, method: 'POST', body: JSON.parse(docString) }, + response: { status: this.response.status, body: this.responseData }, + }, null, 2), 'application/json'); +}); + +When('I send a DELETE request to {string}', async function(endpoint) { + const url = `${this.baseUrl}${endpoint}`; + this.response = await fetch(url, { method: 'DELETE' }); + this.responseData = await this.response.json(); + + // Attach response + this.attach(JSON.stringify({ + url: url, + method: 'DELETE', + status: this.response.status, + }, null, 2), 'application/json'); +}); + +Then('the response status should be {int}', function(expectedStatus) { + assert.strictEqual(this.response.status, expectedStatus, + `Expected status ${expectedStatus} but got ${this.response.status}`); +}); + +Then('the response should contain {int} items', function(count) { + assert.ok(Array.isArray(this.responseData), + `Expected response to be an array but got ${typeof this.responseData}`); + assert.strictEqual(this.responseData.length, count, + `Expected ${count} items but got ${this.responseData.length}`); +}); + +Then('each item should have an {string} field', function(fieldName) { + assert.ok(Array.isArray(this.responseData), 'Response should be an array'); + for (const item of this.responseData) { + assert.ok(item[fieldName] !== undefined, + `Item missing required field: ${fieldName}`); + } +}); + +Then('the response {string} should be {string}', function(field, expected) { + assert.strictEqual(String(this.responseData[field]), expected, + `Expected ${field} to be "${expected}" but got "${this.responseData[field]}"`); +}); + +Then('the response should have an {string} field', function(fieldName) { + assert.ok(this.responseData[fieldName] !== undefined, + `Response missing required field: ${fieldName}`); +}); + +Then('the response should have a {string} field', function(fieldName) { + assert.ok(this.responseData[fieldName] !== undefined, + `Response missing required field: ${fieldName}`); +}); + +Then('the response should be an empty object', function() { + assert.ok(typeof this.responseData === 'object' && !Array.isArray(this.responseData), + 'Response should be an object'); + assert.strictEqual(Object.keys(this.responseData).length, 0, + `Expected empty object but got ${JSON.stringify(this.responseData)}`); +}); + +Then('all items should have {string} equal to {int}', function(field, expected) { + assert.ok(Array.isArray(this.responseData), 'Response should be an array'); + for (const item of this.responseData) { + assert.strictEqual(item[field], expected, + `Expected ${field} to be ${expected} but got ${item[field]}`); + } +}); diff --git a/examples/single/cucumberjs/step_definitions/simple_steps.js b/examples/single/cucumberjs/step_definitions/simple_steps.js deleted file mode 100644 index 1f04e338..00000000 --- a/examples/single/cucumberjs/step_definitions/simple_steps.js +++ /dev/null @@ -1,26 +0,0 @@ -const { Given, When, Then } = require('@cucumber/cucumber'); - -Given('I have a step', function() { - console.log('I have a step'); - this.attach('I\'m an attachment', 'text/plain'); -}); - -Given('I have another step', function() { - console.log('I have another step'); -}); - -When('I do something', function() { - console.log('I do something'); -}); - -Then('I expect something to happen', function() { - console.log('I expect something to happen'); -}); - -Then('I fail', function() { - throw new Error('I fail'); -}); - -When('I fail', function() { - throw new Error('I fail'); -}); diff --git a/examples/single/cucumberjs/step_definitions/table_steps.js b/examples/single/cucumberjs/step_definitions/table_steps.js deleted file mode 100644 index 7367436f..00000000 --- a/examples/single/cucumberjs/step_definitions/table_steps.js +++ /dev/null @@ -1,10 +0,0 @@ -const { Given, Then } = require('@cucumber/cucumber'); - -Given('I have a table with {int} rows', function(rows) { - console.log(`Table with ${rows} rows`); -}); - -Then('the table should have {int} rows', function(rows) { - console.log(`Table with ${rows} rows`); - this.attach('image/png;base64', 'image/png;base64', 'image/png;base64'); -}); diff --git a/examples/single/cypress/README.md b/examples/single/cypress/README.md index 981cf9e5..7e239dce 100644 --- a/examples/single/cypress/README.md +++ b/examples/single/cypress/README.md @@ -1,7 +1,8 @@ -# Cypress Example +# Cypress Example - E-commerce Test Suite -This is a sample project demonstrating how to write and execute tests using the Cypress framework with integration to -Qase Test Management. +## Overview + +This is a realistic e-commerce test suite demonstrating how to write and execute end-to-end tests using Cypress with integration to Qase Test Management. The tests run against [saucedemo.com](https://www.saucedemo.com), a demo e-commerce site, covering authentication, product browsing, shopping cart, and checkout flows using the Page Object Model pattern. ## Prerequisites @@ -10,9 +11,9 @@ Ensure that the following tools are installed on your machine: 1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) 2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) -## Setup Instructions +## Installation -1. Clone this repository by running the following commands: +1. Clone this repository: ```bash git clone https://github.com/qase-tms/qase-javascript.git cd qase-javascript/examples/single/cypress @@ -26,133 +27,269 @@ Ensure that the following tools are installed on your machine: 3. Create a `qase.config.json` file in the root of the project. Follow the instructions on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). -4. To run tests locally without Qase reporting (interactive mode): - ```bash - QASE_MODE=off npx cypress open - ``` +## Configuration -5. To run tests locally without Qase reporting (headless mode): - ```bash - QASE_MODE=off npx cypress run - ``` +The Qase reporter can be configured using environment variables or configuration files. -6. To run tests and upload the results to Qase Test Management: - ```bash - QASE_MODE=testops npx cypress run - ``` +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) -## Example Files +### Option 1: qase.config.json -This project contains several test files demonstrating different Qase features: +```json +{ + "mode": "testops", + "debug": false, + "testops": { + "api": { + "token": "your_api_token_here" + }, + "project": "YOUR_PROJECT_CODE", + "run": { + "title": "Cypress E-commerce Test Run", + "complete": true + } + } +} +``` -| File | Feature | Description | -|------|---------|-------------| -| `simpleTests.cy.js` | Basic tests | Simple Cypress tests with and without Qase integration | -| `methodTests.cy.js` | Qase methods | Demonstrates `qase.comment()` and `qase.attach()` methods | -| `stepTests.cy.js` | Test steps | Defines execution steps with `qase.step()` (synchronous callbacks) | -| `parametrizedTests.cy.js` | Parameters | Reports parameterized test data with `qase.parameters()` | +### Option 2: cypress.config.js -## Expected Behavior +The configuration is already set up in `cypress.config.js` with the Qase reporter. You can customize it further: -### Running with QASE_MODE=off (Local Development) +```javascript +module.exports = defineConfig({ + reporter: 'cypress-multi-reporters', + reporterOptions: { + reporterEnabled: 'cypress-qase-reporter', + cypressQaseReporterReporterOptions: { + debug: true, + testops: { + api: { + token: process.env.QASE_TESTOPS_API_TOKEN, + }, + project: process.env.QASE_TESTOPS_PROJECT, + uploadAttachments: true, + run: { + complete: true, + }, + }, + }, + }, + e2e: { + baseUrl: 'https://www.saucedemo.com', + setupNodeEvents(on, config) { + require('cypress-qase-reporter/plugin')(on, config); + require('cypress-qase-reporter/metadata')(on); + // ... other event handlers + }, + }, +}); +``` + +## Running Tests + +```bash +# Run tests without Qase reporting (default) +npm test + +# Run tests with Qase reporting +QASE_MODE=testops npm test +``` + +### Local Development (Without Qase Reporting) -When running tests with `QASE_MODE=off`, tests execute normally without Qase reporting: +Run tests locally without sending results to Qase: -- Tests run and pass/fail as usual +```bash +# Interactive mode with Cypress UI +QASE_MODE=off npx cypress open + +# Headless mode +QASE_MODE=off npx cypress run +``` + +In this mode: +- Tests execute normally - No data is sent to Qase TestOps - No Qase API token required -- Output shows standard Cypress test results -- Cypress screenshots and videos work normally +- Standard Cypress test output -This mode is useful for local development and debugging. +### CI/CD Mode (With Qase Reporting) -### Running with QASE_MODE=testops (CI/CD and Reporting) +Run tests and upload results to Qase TestOps: -When running tests with `QASE_MODE=testops`, test results are reported to Qase: +```bash +QASE_MODE=testops npx cypress run +``` +In this mode: - Tests execute and results are sent to Qase TestOps - A new test run is created in your Qase project -- Test results include all metadata (steps, attachments, comments, etc.) +- Test results include all metadata (steps, attachments, comments, parameters, etc.) - Console output includes Qase test run link - Requires valid `QASE_TESTOPS_API_TOKEN` and `QASE_TESTOPS_PROJECT` configuration - Cypress screenshots on failure can be attached automatically -**Steps Example (`stepTests.cy.js`):** -- Creates test result with multiple named steps using `qase.step()` -- Each step shows execution status, duration, and any errors -- **Important:** Cypress steps use synchronous callbacks (no async/await) -- Nested steps can be created by calling `qase.step()` within another step -- Steps are visible in Qase test run details +## Test Scenarios -**Attachments Example (`methodTests.cy.js`):** -- Content attached via `qase.attach()` appears in test results -- Supports attaching text, JSON, and other content types -- Cypress screenshots and videos can be attached automatically on failure -- Attachments are visible in the test run details +This example demonstrates realistic e-commerce test scenarios across four test files: -**Parameters Example (`parametrizedTests.cy.js`):** -- Parameterized tests report their parameter values to Qase -- Parameters help identify which test variant produced which result -- Useful for data-driven testing scenarios +| File | Scenarios | Description | +|------|-----------|-------------| +| `login.cy.js` | Authentication | Login with valid/invalid credentials, locked user | +| `inventory.cy.js` | Product Browsing | Browse products, sort by price, view details | +| `cart.cy.js` | Shopping Cart | Add/remove products, multiple items | +| `checkout.cy.js` | Checkout Flow | Complete purchase, validation, cancel checkout | -**Multi-Project Support:** -- When configured for multi-project reporting, same test results are sent to multiple Qase projects -- Each project can have different test case IDs for the same test +**Total:** 13 test cases covering the complete e-commerce user journey. -## Configuration +## Qase Features Demonstrated -Example `qase.config.json`: +This example demonstrates all Qase reporter features in realistic test scenarios: -```json -{ - "mode": "testops", - "debug": false, - "testops": { - "api": { - "token": "your_api_token_here" - }, - "project": "YOUR_PROJECT_CODE", - "run": { - "title": "Cypress Automated Test Run", - "complete": true - } - } -} +| Feature | API Method | Example Location | Description | +|---------|-----------|------------------|-------------| +| **Test ID** | `qase(id, it(...))` | All tests | Link Cypress tests to Qase test cases | +| **Title** | `it('title', ...)` | All tests | Test case title (from Cypress test name) | +| **Fields** | `qase.fields({...})` | All tests | Severity, priority, layer metadata | +| **Suite** | `qase.suite('...')` | All tests | Hierarchical test organization using `\t` separator | +| **Steps** | `qase.step('name', () => {...})` | All tests | Named test execution steps | +| **Attachments** | `qase.attach({...})` | inventory, cart, checkout | Attach JSON, text, or other content | +| **Comments** | `qase.comment('...')` | All tests | Additional context for test results | +| **Parameters** | `qase.parameters({...})` | login, inventory, checkout | Report test parameters/inputs | +| **Ignore** | `qase.ignore()` | checkout test 13 | Exclude specific tests from reporting | + +### Example: Complete Test with Multiple Features + +```javascript +qase(10, + it('User can complete checkout with valid information', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete Flow'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + qase.step('Fill in checkout information', () => { + CheckoutPage.fillInfo('John', 'Doe', '12345'); + }); + + qase.step('Complete the order', () => { + CheckoutPage.finish(); + }); + + qase.step('Attach order details', () => { + qase.attach({ + name: 'order-details.txt', + content: 'Order Details: ...', + contentType: 'text/plain' + }); + }); + + qase.comment('Checkout completed successfully'); + }) +); ``` -Or configure via `cypress.config.js`: +## Cypress-Specific Patterns + +### Synchronous Steps (CRITICAL) + +Unlike Jest or Playwright, Cypress steps use **synchronous callbacks**. Do NOT use `async/await` with `qase.step()`: ```javascript -const { defineConfig } = require('cypress'); +// ✅ CORRECT - Synchronous callback +qase.step('Add product to cart', () => { + InventoryPage.addToCart('sauce-labs-backpack'); + InventoryPage.getCartBadge().should('have.text', '1'); +}); -module.exports = defineConfig({ - reporter: 'cypress-qase-reporter', - reporterOptions: { - mode: 'testops', - testops: { - api: { - token: process.env.QASE_TESTOPS_API_TOKEN, - }, - project: 'YOUR_PROJECT_CODE', - run: { - complete: true, - }, - }, - }, - e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, - }, +// ❌ WRONG - Do not use async/await +qase.step('Add product to cart', async () => { // ❌ NO async + await InventoryPage.addToCart('...'); // ❌ NO await +}); +``` + +Cypress commands are already queued and executed asynchronously by Cypress itself. Adding `async/await` will break the Cypress command chain. + +### Import Pattern + +Use the `/mocha` import path, as Cypress uses Mocha under the hood: + +```javascript +import { qase } from 'cypress-qase-reporter/mocha'; // ✅ CORRECT +``` + +### Attachment Content Type + +Use `contentType` parameter (not `type`) for attachments: + +```javascript +qase.attach({ + name: 'data.json', + content: JSON.stringify({ ... }), + contentType: 'application/json' // ✅ Use contentType }); ``` -## Important Notes +### Test ID Wrapper Pattern + +Use the wrapper pattern to link test IDs: + +```javascript +qase(1, // ✅ Wrap the entire it() call + it('Test name', () => { + // test implementation + }) +); +``` + +### Suite Hierarchy -- **Synchronous Steps:** Unlike Jest or Playwright, Cypress steps use synchronous callbacks. Do NOT use `async/await` with `qase.step()` in Cypress tests. -- **Import Pattern:** Use `import { qase } from 'cypress-qase-reporter/mocha';` (note the `/mocha` suffix, as Cypress uses Mocha under the hood) +Use `\t` (tab character) to define suite hierarchy: + +```javascript +qase.suite('E-commerce\tCheckout\tValidation'); +// Creates: E-commerce > Checkout > Validation +``` + +## Project Structure + +``` +cypress/ +├── e2e/ +│ ├── login.cy.js # Authentication test scenarios +│ ├── inventory.cy.js # Product browsing test scenarios +│ ├── cart.cy.js # Shopping cart test scenarios +│ └── checkout.cy.js # Checkout test scenarios +├── support/ +│ ├── pages/ +│ │ ├── LoginPage.js # Authentication page interactions +│ │ ├── InventoryPage.js # Product browsing and cart operations +│ │ ├── CartPage.js # Shopping cart management +│ │ └── CheckoutPage.js # Checkout flow operations +│ ├── commands.js # Custom commands (login helper) +│ └── e2e.js # Global configuration +├── cypress.config.js # Cypress configuration +└── qase.config.json # Qase reporter configuration +``` + +Page objects are located in `cypress/support/pages/` and follow Cypress patterns (synchronous operations, returning `cy` chains). + +## Custom Commands + +This example includes a custom `login` command for convenience: + +```javascript +// Usage in tests +cy.login(); // Logs in with default credentials (standard_user/secret_sauce) +cy.login('problem_user', 'secret_sauce'); // Logs in with custom credentials +``` ## Additional Resources -For more details on how to use this integration with Qase Test Management, visit -the [Qase Cypress documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-cypress). +- [Qase Cypress Reporter Documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-cypress) +- [Cypress Documentation](https://docs.cypress.io/) +- [Saucedemo Test Site](https://www.saucedemo.com) +- [Qase TestOps](https://qase.io/) diff --git a/examples/single/cypress/cypress.config.js b/examples/single/cypress/cypress.config.js index bb9214e7..46084c21 100644 --- a/examples/single/cypress/cypress.config.js +++ b/examples/single/cypress/cypress.config.js @@ -35,6 +35,7 @@ module.exports = cypress.defineConfig({ video: true, screenshotOnRunFailure: true, e2e: { + baseUrl: 'https://www.saucedemo.com', setupNodeEvents(on, config) { require('cypress-qase-reporter/plugin')(on, config); require('cypress-qase-reporter/metadata')(on); diff --git a/examples/single/cypress/cypress/e2e/cart.cy.js b/examples/single/cypress/cypress/e2e/cart.cy.js new file mode 100644 index 00000000..b81dae72 --- /dev/null +++ b/examples/single/cypress/cypress/e2e/cart.cy.js @@ -0,0 +1,111 @@ +import { qase } from 'cypress-qase-reporter/mocha'; +import InventoryPage from '../support/pages/InventoryPage'; +import CartPage from '../support/pages/CartPage'; + +describe('Cart Management', () => { + beforeEach(() => { + cy.login(); + }); + + qase(7, + it('User can add product to cart', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tShopping Cart\tAdd Items'); + qase.parameters({ product: 'Sauce Labs Backpack' }); + + qase.step('Verify cart is initially empty', () => { + cy.get('.shopping_cart_badge').should('not.exist'); + }); + + qase.step('Add Sauce Labs Backpack to cart', () => { + InventoryPage.addToCart('sauce-labs-backpack'); + }); + + qase.step('Verify cart badge shows 1 item', () => { + InventoryPage.getCartBadge().should('have.text', '1'); + }); + + qase.step('Navigate to cart', () => { + InventoryPage.goToCart(); + }); + + qase.step('Verify product appears in cart', () => { + cy.url().should('include', '/cart.html'); + CartPage.getItems().should('have.length', 1); + CartPage.getItemName().should('contain.text', 'Sauce Labs Backpack'); + }); + + qase.step('Attach cart state', () => { + CartPage.getItems().then(($items) => { + const cartState = { + itemCount: $items.length, + items: ['Sauce Labs Backpack'], + timestamp: new Date().toISOString() + }; + qase.attach({ + name: 'cart-state.json', + content: JSON.stringify(cartState, null, 2), + contentType: 'application/json' + }); + }); + }); + + qase.comment('Successfully added product to cart'); + }) + ); + + qase(8, + it('User can remove product from cart', () => { + qase.fields({ severity: 'major', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tShopping Cart\tRemove Items'); + + qase.step('Add product to cart', () => { + InventoryPage.addToCart('sauce-labs-bike-light'); + InventoryPage.getCartBadge().should('have.text', '1'); + }); + + qase.step('Navigate to cart', () => { + InventoryPage.goToCart(); + }); + + qase.step('Verify product is in cart', () => { + CartPage.getItems().should('have.length', 1); + }); + + qase.step('Remove product from cart', () => { + CartPage.removeItem('sauce-labs-bike-light'); + }); + + qase.step('Verify cart is empty', () => { + CartPage.getItems().should('have.length', 0); + cy.get('.shopping_cart_badge').should('not.exist'); + }); + + qase.comment('Successfully removed product from cart'); + }) + ); + + qase(9, + it('User can add multiple products to cart', () => { + qase.fields({ severity: 'major', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tShopping Cart\tMultiple Items'); + + qase.step('Add first product to cart', () => { + InventoryPage.addToCart('sauce-labs-backpack'); + InventoryPage.getCartBadge().should('have.text', '1'); + }); + + qase.step('Add second product to cart', () => { + InventoryPage.addToCart('sauce-labs-bolt-t-shirt'); + InventoryPage.getCartBadge().should('have.text', '2'); + }); + + qase.step('Navigate to cart and verify both products', () => { + InventoryPage.goToCart(); + CartPage.getItems().should('have.length', 2); + }); + + qase.comment('Multiple products added successfully'); + }) + ); +}); diff --git a/examples/single/cypress/cypress/e2e/checkout.cy.js b/examples/single/cypress/cypress/e2e/checkout.cy.js new file mode 100644 index 00000000..af41f9df --- /dev/null +++ b/examples/single/cypress/cypress/e2e/checkout.cy.js @@ -0,0 +1,145 @@ +import { qase } from 'cypress-qase-reporter/mocha'; +import InventoryPage from '../support/pages/InventoryPage'; +import CartPage from '../support/pages/CartPage'; +import CheckoutPage from '../support/pages/CheckoutPage'; + +describe('Checkout Flow', () => { + beforeEach(() => { + cy.login(); + InventoryPage.addToCart('sauce-labs-backpack'); + InventoryPage.goToCart(); + CartPage.checkout(); + }); + + qase(10, + it('User can complete checkout with valid information', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete Flow'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + qase.step('Verify on checkout information page', () => { + cy.url().should('include', '/checkout-step-one.html'); + CheckoutPage.getTitle().should('have.text', 'Checkout: Your Information'); + }); + + qase.step('Fill in checkout information', () => { + CheckoutPage.fillInfo('John', 'Doe', '12345'); + }); + + qase.step('Continue to overview', () => { + CheckoutPage.continue(); + }); + + qase.step('Verify on checkout overview page', () => { + cy.url().should('include', '/checkout-step-two.html'); + CheckoutPage.getTitle().should('have.text', 'Checkout: Overview'); + }); + + qase.step('Verify order details are correct', () => { + cy.get('.cart_item').should('have.length', 1); + cy.get('[data-test="inventory-item-name"]').should('contain.text', 'Sauce Labs Backpack'); + cy.get('.summary_total_label').should('be.visible'); + }); + + qase.step('Complete the order', () => { + CheckoutPage.finish(); + }); + + qase.step('Verify order completion', () => { + cy.url().should('include', '/checkout-complete.html'); + CheckoutPage.getCompleteHeader().should('have.text', 'Thank you for your order!'); + }); + + qase.step('Attach order details', () => { + const orderDetails = `Order Details: +Customer: John Doe +Postal Code: 12345 +Product: Sauce Labs Backpack +Order Date: ${new Date().toISOString()} +Status: Complete`; + + qase.attach({ + name: 'order-details.txt', + content: orderDetails, + contentType: 'text/plain' + }); + }); + + qase.comment('Checkout completed successfully'); + }) + ); + + qase(11, + it('Checkout fails without required information', () => { + qase.fields({ severity: 'major', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tValidation'); + qase.parameters({ scenario: 'missing_first_name' }); + + qase.step('Verify on checkout information page', () => { + cy.url().should('include', '/checkout-step-one.html'); + }); + + qase.step('Fill only last name and postal code', () => { + CheckoutPage.fillLastName('Smith'); + CheckoutPage.fillPostalCode('54321'); + }); + + qase.step('Attempt to continue without first name', () => { + CheckoutPage.continue(); + }); + + qase.step('Verify error message is displayed', () => { + CheckoutPage.getError() + .should('be.visible') + .and('contain.text', 'Error: First Name is required'); + }); + + qase.step('Verify still on information page', () => { + cy.url().should('include', '/checkout-step-one.html'); + }); + + qase.comment('Validation working correctly'); + }) + ); + + qase(12, + it('User can cancel checkout', () => { + qase.fields({ severity: 'normal', priority: 'low', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tNavigation'); + + qase.step('Verify on checkout information page', () => { + cy.url().should('include', '/checkout-step-one.html'); + }); + + qase.step('Click cancel button', () => { + CheckoutPage.cancel(); + }); + + qase.step('Verify returned to cart page', () => { + cy.url().should('include', '/cart.html'); + CartPage.getTitle().should('have.text', 'Your Cart'); + }); + + qase.step('Verify product still in cart', () => { + CartPage.getItems().should('have.length', 1); + }); + + qase.comment('Cancel navigation works correctly'); + }) + ); + + qase(13, + it('Demo test that will be ignored in reporting', () => { + qase.ignore(); + qase.fields({ severity: 'minor', priority: 'low', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tDemo'); + + // This test demonstrates qase.ignore() feature + // It will run but won't be reported to Qase TestOps + cy.log('This test is ignored in Qase reporting'); + cy.url().should('include', '/checkout-step-one.html'); + + qase.comment('This test is intentionally ignored for demonstration purposes'); + }) + ); +}); diff --git a/examples/single/cypress/cypress/e2e/inventory.cy.js b/examples/single/cypress/cypress/e2e/inventory.cy.js new file mode 100644 index 00000000..6d1f1358 --- /dev/null +++ b/examples/single/cypress/cypress/e2e/inventory.cy.js @@ -0,0 +1,97 @@ +import { qase } from 'cypress-qase-reporter/mocha'; +import InventoryPage from '../support/pages/InventoryPage'; + +describe('Product Inventory', () => { + beforeEach(() => { + cy.login(); + cy.url().should('include', '/inventory.html'); + }); + + qase(4, + it('User can browse all products', () => { + qase.fields({ severity: 'major', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tInventory\tBrowsing'); + + let productCount = 0; + + qase.step('Verify Products page title', () => { + InventoryPage.getTitle().should('have.text', 'Products'); + }); + + qase.step('Count inventory items', () => { + InventoryPage.getItems().should('have.length', 6).then(($items) => { + productCount = $items.length; + }); + }); + + qase.step('Verify product names are visible', () => { + InventoryPage.getItemNames().each(($name) => { + cy.wrap($name).should('be.visible'); + }); + }); + + qase.step('Verify product prices are visible', () => { + InventoryPage.getItemPrices().each(($price) => { + cy.wrap($price).should('be.visible').and('contain.text', '$'); + }); + }); + + qase.step('Attach product count data', () => { + qase.attach({ + name: 'product-count.json', + content: JSON.stringify({ totalProducts: productCount, timestamp: new Date().toISOString() }, null, 2), + contentType: 'application/json' + }); + }); + + qase.comment('Successfully browsed all 6 products on inventory page'); + }) + ); + + qase(5, + it('User can sort products by price', () => { + qase.fields({ severity: 'normal', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tInventory\tSorting'); + qase.parameters({ sortOption: 'lohi' }); + + qase.step('Select sort by price low to high', () => { + InventoryPage.sortBy('lohi'); + }); + + qase.step('Verify prices are in ascending order', () => { + const prices = []; + InventoryPage.getItemPrices().each(($price) => { + const priceText = $price.text().replace('$', ''); + prices.push(parseFloat(priceText)); + }).then(() => { + const sortedPrices = [...prices].sort((a, b) => a - b); + expect(prices).to.deep.equal(sortedPrices); + }); + }); + + qase.comment('Products sorted correctly by price'); + }) + ); + + qase(6, + it('User can view product details', () => { + qase.fields({ severity: 'normal', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tInventory\tProduct Details'); + + qase.step('Click on first product name', () => { + InventoryPage.getItemNames().first().click(); + }); + + qase.step('Verify product detail page loads', () => { + cy.url().should('include', '/inventory-item.html'); + cy.get('.inventory_details_name').should('be.visible'); + cy.get('.inventory_details_desc').should('be.visible'); + cy.get('.inventory_details_price').should('be.visible'); + }); + + qase.step('Verify back to products button is present', () => { + cy.get('[data-test="back-to-products"]').should('be.visible'); + }); + }) + ); +}); diff --git a/examples/single/cypress/cypress/e2e/login.cy.js b/examples/single/cypress/cypress/e2e/login.cy.js new file mode 100644 index 00000000..3e199b22 --- /dev/null +++ b/examples/single/cypress/cypress/e2e/login.cy.js @@ -0,0 +1,83 @@ +import { qase } from 'cypress-qase-reporter/mocha'; +import LoginPage from '../support/pages/LoginPage'; +import InventoryPage from '../support/pages/InventoryPage'; + +describe('Login Scenarios', () => { + beforeEach(() => { + LoginPage.visit(); + }); + + qase(1, + it('User can login with valid credentials', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + + qase.step('Fill in username', () => { + LoginPage.fillUsername('standard_user'); + }); + + qase.step('Fill in password', () => { + LoginPage.fillPassword('secret_sauce'); + }); + + qase.step('Submit login form', () => { + LoginPage.submit(); + }); + + qase.step('Verify successful login', () => { + cy.url().should('include', '/inventory.html'); + InventoryPage.getTitle().should('have.text', 'Products'); + }); + + qase.comment('Login successful'); + }) + ); + + qase(2, + it('User cannot login with invalid password', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'standard_user', password: 'wrong_password' }); + + qase.step('Attempt login with invalid credentials', () => { + LoginPage.fillUsername('standard_user'); + LoginPage.fillPassword('wrong_password'); + LoginPage.submit(); + }); + + qase.step('Verify error message is shown', () => { + LoginPage.getError() + .should('be.visible') + .and('contain.text', 'Username and password do not match'); + }); + + qase.step('Verify still on login page', () => { + cy.url().should('not.include', '/inventory.html'); + }); + }) + ); + + qase(3, + it('Locked user cannot login', () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'locked_out_user', password: 'secret_sauce' }); + + qase.step('Attempt login with locked user', () => { + LoginPage.fillUsername('locked_out_user'); + LoginPage.fillPassword('secret_sauce'); + LoginPage.submit(); + }); + + qase.step('Verify locked out error message', () => { + LoginPage.getError() + .should('be.visible') + .and('contain.text', 'Sorry, this user has been locked out'); + }); + + qase.step('Verify cannot access inventory', () => { + cy.url().should('not.include', '/inventory.html'); + }); + }) + ); +}); diff --git a/examples/single/cypress/cypress/e2e/methodTests.cy.js b/examples/single/cypress/cypress/e2e/methodTests.cy.js deleted file mode 100644 index ca6cedeb..00000000 --- a/examples/single/cypress/cypress/e2e/methodTests.cy.js +++ /dev/null @@ -1,31 +0,0 @@ -import { qase } from 'cypress-qase-reporter/mocha'; - -describe('Method tests', () => { - it('test with comment success', () => { - qase.comment('My comment'); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test with comment failed', () => { - qase.comment('My comment'); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); - - it('test with attach success', () => { - qase.attach({ name: 'log.txt', content: 'My content', contentType: 'text/plain' }); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test with attach failed', () => { - qase.attach({ name: 'log.txt', content: 'My content', contentType: 'text/plain' }); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); -}); diff --git a/examples/single/cypress/cypress/e2e/parametrizedTests.cy.js b/examples/single/cypress/cypress/e2e/parametrizedTests.cy.js deleted file mode 100644 index a8550c89..00000000 --- a/examples/single/cypress/cypress/e2e/parametrizedTests.cy.js +++ /dev/null @@ -1,21 +0,0 @@ -import { qase } from 'cypress-qase-reporter/mocha'; - -describe('Parametrized tests', () => { - let ids = [1, 2, 3, 4, 5]; - - for (let i = 0; i < ids.length; i++) { - it(`Test with parameter success`, () => { - qase.parameters({ 'some_parameter': ids[i].toString() }); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it(`Test with parameter failed`, () => { - qase.parameters({ 'some_parameter': ids[i].toString() }); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); - } -}); diff --git a/examples/single/cypress/cypress/e2e/simpleTests.cy.js b/examples/single/cypress/cypress/e2e/simpleTests.cy.js deleted file mode 100644 index 36aa6539..00000000 --- a/examples/single/cypress/cypress/e2e/simpleTests.cy.js +++ /dev/null @@ -1,84 +0,0 @@ -import { qase } from 'cypress-qase-reporter/mocha'; - -describe('Simple tests', () => { - - it('test without metadata success', () => { - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test without metadata failed', () => { - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); - - qase(1, it('test with Qase ID success', () => { - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - })); - - qase(2, it('test with Qase ID failed', () => { - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - })); - - it('test with title success', () => { - qase.title('Test with title success'); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test with title failed', () => { - qase.title('Test with title failed'); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); - - it('test with fields success', () => { - qase.fields({ 'description': 'My description' }); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test with fields failed', () => { - qase.fields({ 'description': 'My description' }); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); - - it('test with ignore success', () => { - qase.ignore(); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test with ignore failed', () => { - qase.ignore(); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); - - it('test with suite success', () => { - qase.suite('My suite'); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actions'); - }); - - it('test with suite failed', () => { - qase.suite('My suite'); - cy.visit('https://example.cypress.io'); - cy.contains('type').click(); - cy.url().should('include', '/commands/actionsss'); - }); -}); diff --git a/examples/single/cypress/cypress/e2e/stepTests.cy.js b/examples/single/cypress/cypress/e2e/stepTests.cy.js deleted file mode 100644 index 7054e2db..00000000 --- a/examples/single/cypress/cypress/e2e/stepTests.cy.js +++ /dev/null @@ -1,27 +0,0 @@ -import { qase } from 'cypress-qase-reporter/mocha'; - -describe('Step tests', () => { - it('test with steps success', () => { - qase.step('Step 1', () => { - cy.visit('https://example.cypress.io'); - }); - qase.step('Step 2', () => { - cy.contains('type').click(); - }); - qase.step('Step 3', () => { - cy.url().should('include', '/commands/actions'); - }); - }); - - it('test with steps failed', () => { - qase.step('Step 1', () => { - cy.visit('https://example.cypress.io'); - }); - qase.step('Step 2', () => { - cy.contains('type').click(); - }); - qase.step('Step 3', () => { - cy.url().should('include', '/commands/actionsss'); - }); - }); -}); diff --git a/examples/single/cypress/cypress/fixtures/example.json b/examples/single/cypress/cypress/fixtures/example.json deleted file mode 100644 index 02e42543..00000000 --- a/examples/single/cypress/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/examples/single/cypress/cypress/plugins/index.js b/examples/single/cypress/cypress/plugins/index.js deleted file mode 100644 index 8229063a..00000000 --- a/examples/single/cypress/cypress/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/// -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -// eslint-disable-next-line no-unused-vars -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -}; diff --git a/examples/single/cypress/cypress/support/commands.js b/examples/single/cypress/cypress/support/commands.js index 22e1006b..738020f0 100644 --- a/examples/single/cypress/cypress/support/commands.js +++ b/examples/single/cypress/cypress/support/commands.js @@ -23,3 +23,14 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +/** + * Custom login command for saucedemo.com + * Logs in with default or provided credentials + */ +Cypress.Commands.add('login', (username = 'standard_user', password = 'secret_sauce') => { + cy.visit('https://www.saucedemo.com'); + cy.get('[data-test="username"]').type(username); + cy.get('[data-test="password"]').type(password); + cy.get('[data-test="login-button"]').click(); +}); diff --git a/examples/single/cypress/cypress/support/pages/CartPage.js b/examples/single/cypress/cypress/support/pages/CartPage.js new file mode 100644 index 00000000..db6fc181 --- /dev/null +++ b/examples/single/cypress/cypress/support/pages/CartPage.js @@ -0,0 +1,57 @@ +/** + * Cart Page Object for saucedemo.com + * + * This page object encapsulates interactions with the shopping cart page. + * Cypress page objects are simple classes that wrap cy commands. + * They do NOT use async/await and return cy chains for further chaining. + */ +class CartPage { + /** + * Get all cart items + * @returns {Cypress.Chainable} All cart items + */ + getItems() { + return cy.get('.cart_item'); + } + + /** + * Get the product name in cart + * @returns {Cypress.Chainable} The product name element + */ + getItemName() { + return cy.get('[data-test="inventory-item-name"]'); + } + + /** + * Remove a product from cart by its slug + * @param {string} productSlug - The product slug (e.g., 'sauce-labs-backpack') + */ + removeItem(productSlug) { + return cy.get(`[data-test="remove-${productSlug}"]`).click(); + } + + /** + * Proceed to checkout + */ + checkout() { + return cy.get('[data-test="checkout"]').click(); + } + + /** + * Continue shopping (return to inventory) + */ + continueShopping() { + return cy.get('[data-test="continue-shopping"]').click(); + } + + /** + * Get the page title + * @returns {Cypress.Chainable} The title element + */ + getTitle() { + return cy.get('.title'); + } +} + +// Export singleton instance +export default new CartPage(); diff --git a/examples/single/cypress/cypress/support/pages/CheckoutPage.js b/examples/single/cypress/cypress/support/pages/CheckoutPage.js new file mode 100644 index 00000000..1644ada0 --- /dev/null +++ b/examples/single/cypress/cypress/support/pages/CheckoutPage.js @@ -0,0 +1,92 @@ +/** + * Checkout Page Object for saucedemo.com + * + * This page object encapsulates interactions with the checkout flow pages. + * Cypress page objects are simple classes that wrap cy commands. + * They do NOT use async/await and return cy chains for further chaining. + */ +class CheckoutPage { + /** + * Fill in the first name field + * @param {string} name - The first name to enter + */ + fillFirstName(name) { + return cy.get('[data-test="firstName"]').type(name); + } + + /** + * Fill in the last name field + * @param {string} name - The last name to enter + */ + fillLastName(name) { + return cy.get('[data-test="lastName"]').type(name); + } + + /** + * Fill in the postal code field + * @param {string} code - The postal code to enter + */ + fillPostalCode(code) { + return cy.get('[data-test="postalCode"]').type(code); + } + + /** + * Fill in all checkout information fields + * @param {string} first - First name + * @param {string} last - Last name + * @param {string} zip - Postal code + */ + fillInfo(first, last, zip) { + this.fillFirstName(first); + this.fillLastName(last); + this.fillPostalCode(zip); + } + + /** + * Continue to the next step + */ + continue() { + return cy.get('[data-test="continue"]').click(); + } + + /** + * Cancel checkout and return to cart + */ + cancel() { + return cy.get('[data-test="cancel"]').click(); + } + + /** + * Complete the order + */ + finish() { + return cy.get('[data-test="finish"]').click(); + } + + /** + * Get the checkout complete header message + * @returns {Cypress.Chainable} The complete header element + */ + getCompleteHeader() { + return cy.get('.complete-header'); + } + + /** + * Get the error message element + * @returns {Cypress.Chainable} The error element + */ + getError() { + return cy.get('[data-test="error"]'); + } + + /** + * Get the page title + * @returns {Cypress.Chainable} The title element + */ + getTitle() { + return cy.get('.title'); + } +} + +// Export singleton instance +export default new CheckoutPage(); diff --git a/examples/single/cypress/cypress/support/pages/InventoryPage.js b/examples/single/cypress/cypress/support/pages/InventoryPage.js new file mode 100644 index 00000000..e6e49aca --- /dev/null +++ b/examples/single/cypress/cypress/support/pages/InventoryPage.js @@ -0,0 +1,82 @@ +/** + * Inventory Page Object for saucedemo.com + * + * This page object encapsulates interactions with the product inventory page. + * Cypress page objects are simple classes that wrap cy commands. + * They do NOT use async/await and return cy chains for further chaining. + */ +class InventoryPage { + /** + * Get all inventory items + * @returns {Cypress.Chainable} All inventory items + */ + getItems() { + return cy.get('.inventory_item'); + } + + /** + * Get all product names + * @returns {Cypress.Chainable} All product name elements + */ + getItemNames() { + return cy.get('[data-test="inventory-item-name"]'); + } + + /** + * Get all product prices + * @returns {Cypress.Chainable} All product price elements + */ + getItemPrices() { + return cy.get('[data-test="inventory-item-price"]'); + } + + /** + * Add a product to cart by its slug + * @param {string} productSlug - The product slug (e.g., 'sauce-labs-backpack') + */ + addToCart(productSlug) { + return cy.get(`[data-test="add-to-cart-${productSlug}"]`).click(); + } + + /** + * Remove a product from cart by its slug + * @param {string} productSlug - The product slug (e.g., 'sauce-labs-backpack') + */ + removeFromCart(productSlug) { + return cy.get(`[data-test="remove-${productSlug}"]`).click(); + } + + /** + * Get the shopping cart badge (displays number of items) + * @returns {Cypress.Chainable} The cart badge element + */ + getCartBadge() { + return cy.get('.shopping_cart_badge'); + } + + /** + * Navigate to the shopping cart + */ + goToCart() { + return cy.get('#shopping_cart_container a').click(); + } + + /** + * Sort products by the given option + * @param {string} value - Sort option value (e.g., 'az', 'za', 'lohi', 'hilo') + */ + sortBy(value) { + return cy.get('.product_sort_container').select(value); + } + + /** + * Get the page title + * @returns {Cypress.Chainable} The title element + */ + getTitle() { + return cy.get('.title'); + } +} + +// Export singleton instance +export default new InventoryPage(); diff --git a/examples/single/cypress/cypress/support/pages/LoginPage.js b/examples/single/cypress/cypress/support/pages/LoginPage.js new file mode 100644 index 00000000..89959e0f --- /dev/null +++ b/examples/single/cypress/cypress/support/pages/LoginPage.js @@ -0,0 +1,60 @@ +/** + * Login Page Object for saucedemo.com + * + * This page object encapsulates the login page interactions. + * Cypress page objects are simple classes that wrap cy commands. + * They do NOT use async/await and return cy chains for further chaining. + */ +class LoginPage { + /** + * Navigate to the login page + */ + visit() { + return cy.visit('https://www.saucedemo.com'); + } + + /** + * Fill in the username field + * @param {string} username - The username to enter + */ + fillUsername(username) { + return cy.get('[data-test="username"]').type(username); + } + + /** + * Fill in the password field + * @param {string} password - The password to enter + */ + fillPassword(password) { + return cy.get('[data-test="password"]').type(password); + } + + /** + * Click the login button + */ + submit() { + return cy.get('[data-test="login-button"]').click(); + } + + /** + * Complete login flow + * @param {string} username - The username to use + * @param {string} password - The password to use + */ + login(username, password) { + this.fillUsername(username); + this.fillPassword(password); + this.submit(); + } + + /** + * Get the error message element + * @returns {Cypress.Chainable} The error element + */ + getError() { + return cy.get('[data-test="error"]'); + } +} + +// Export singleton instance +export default new LoginPage(); diff --git a/examples/single/cypress/package.json b/examples/single/cypress/package.json index 7d2133d5..966467d0 100644 --- a/examples/single/cypress/package.json +++ b/examples/single/cypress/package.json @@ -2,7 +2,7 @@ "name": "examples-cypress", "private": true, "scripts": { - "test": "QASE_MODE=testops cypress run" + "test": "QASE_MODE=${QASE_MODE:-off} cypress run" }, "devDependencies": { "cypress": "^15.6.0", diff --git a/examples/single/jest/README.md b/examples/single/jest/README.md index dab633bd..3df0476f 100644 --- a/examples/single/jest/README.md +++ b/examples/single/jest/README.md @@ -1,16 +1,17 @@ -# Jest Example +# Jest Example - API Testing with JSONPlaceholder -This is a sample project demonstrating how to write and execute tests using the Jest framework with integration to -Qase Test Management. +## Overview + +This example project demonstrates how to write realistic API tests using the Jest framework with integration to Qase Test Management. The tests use [JSONPlaceholder](https://jsonplaceholder.typicode.com), a free fake REST API for testing and prototyping, covering CRUD operations, post validation, error handling, and advanced features like nested steps and suite hierarchies. ## Prerequisites Ensure that the following tools are installed on your machine: -1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) +1. [Node.js](https://nodejs.org/) (version 18 or higher is required for native fetch support) 2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) -## Setup Instructions +## Installation 1. Clone this repository by running the following commands: ```bash @@ -26,72 +27,14 @@ Ensure that the following tools are installed on your machine: 3. Create a `qase.config.json` file in the root of the project. Follow the instructions on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). -4. To run tests locally without Qase reporting: - ```bash - QASE_MODE=off npm test - ``` - -5. To run tests and upload the results to Qase Test Management: - ```bash - QASE_MODE=testops npm test - ``` - -## Example Files - -This project contains several test files demonstrating different Qase features: - -| File | Feature | Description | -|------|---------|-------------| -| `id.test.js` | Test case linking | Links test to Qase test case by ID using `qase(id, name)` wrapper | -| `title.test.js` | Custom titles | Sets custom test result titles with `qase.title()` | -| `fields.test.js` | Custom fields | Sets severity, priority, description, and other metadata with `qase.fields()` | -| `suite.test.js` | Suite organization | Groups tests into suites and sub-suites with `qase.suite()` | -| `steps.test.js` | Test steps | Defines execution steps with `await qase.step()` for detailed reporting | -| `attach.test.js` | Attachments | Attaches files and content to test results with `qase.attach()` | -| `comment.test.js` | Comments | Adds comments to test results with `qase.comment()` | -| `ignore.test.js` | Ignoring tests | Excludes tests from Qase reporting with `qase.ignore()` | -| `params.test.js` | Parameters | Reports parameterized test data with `qase.parameters()` | - -## Expected Behavior - -### Running with QASE_MODE=off (Local Development) - -When running tests with `QASE_MODE=off`, tests execute normally without Qase reporting: - -- Tests run and pass/fail as usual -- No data is sent to Qase TestOps -- No Qase API token required -- Output shows standard Jest test results - -This mode is useful for local development and debugging. - -### Running with QASE_MODE=testops (CI/CD and Reporting) - -When running tests with `QASE_MODE=testops`, test results are reported to Qase: - -- Tests execute and results are sent to Qase TestOps -- A new test run is created in your Qase project -- Test results include all metadata (steps, attachments, fields, etc.) -- Console output includes Qase test run link -- Requires valid `QASE_TESTOPS_API_TOKEN` and `QASE_TESTOPS_PROJECT` configuration - -**Steps Example (`steps.test.js`):** -- Creates test result with multiple named steps -- Each step shows execution status, duration, and any errors -- Nested steps appear hierarchically in Qase -- Steps with expected results and data are captured - -**Attachments Example (`attach.test.js`):** -- Files attached via `paths` option appear in test results -- Content attached via `content` option is uploaded to Qase -- Attachments are visible in the test run details -- Supports text, JSON, images, and binary files +## Configuration -**Multi-Project Support:** -- When configured for multi-project reporting, same test results are sent to multiple Qase projects -- Each project can have different test case IDs for the same test +The Qase reporter can be configured using environment variables or configuration files. -## Configuration +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) Example `qase.config.json`: @@ -105,7 +48,7 @@ Example `qase.config.json`: }, "project": "YOUR_PROJECT_CODE", "run": { - "title": "Jest Automated Test Run", + "title": "Jest API Test Run", "complete": true } } @@ -116,6 +59,7 @@ Or configure via `jest.config.js`: ```javascript module.exports = { + testTimeout: 10000, // API requests may take longer reporters: [ 'default', [ @@ -137,6 +81,180 @@ module.exports = { }; ``` +## Running Tests + +```bash +# Run tests without Qase reporting (default) +npm test + +# Run tests with Qase reporting +QASE_MODE=testops npm test +``` + +### Run specific test file + +```bash +npm test -- api-crud.test.js +``` + +### Run tests with verbose output + +```bash +npm test -- --verbose +``` + +### Expected Behavior + +**Running with QASE_MODE=off (Local Development)** + +When running tests with `QASE_MODE=off`, tests execute normally without Qase reporting: + +- Tests run and pass/fail as usual +- Real HTTP requests are made to JSONPlaceholder API +- No data is sent to Qase TestOps +- No Qase API token required +- Output shows standard Jest test results + +**Running with QASE_MODE=testops (CI/CD and Reporting)** + +When running tests with `QASE_MODE=testops`, test results are reported to Qase: + +- Tests execute with real API calls to JSONPlaceholder +- Results are sent to Qase TestOps with all metadata +- A new test run is created in your Qase project +- Console output includes Qase test run link +- All steps, attachments, fields, and comments are captured +- Requires valid `QASE_TESTOPS_API_TOKEN` and `QASE_TESTOPS_PROJECT` configuration + +## Test Scenarios + +This project contains 4 test files with realistic API testing scenarios: + +### api-crud.test.js - User CRUD Operations + +Tests CRUD (Create, Read, Update, Delete) operations on the users endpoint: +- GET all users (verify 10 users returned) +- GET single user by ID (verify user details) +- POST create new user (verify 201 response and ID assignment) +- DELETE user (verify 200 response) + +**Qase features demonstrated:** `qase.id`, `qase.fields`, `qase.step`, `qase.parameters`, `qase.attach`, `qase.comment` + +### api-posts.test.js - Post Validation + +Tests post retrieval and validation with filtering: +- GET all posts (verify 100 posts returned) +- GET posts filtered by user ID (verify query parameters) +- GET post with comments (verify nested data structure) + +**Qase features demonstrated:** `qase.id`, `qase.fields`, `qase.parameters`, `qase.step`, `qase.attach` + +### api-errors.test.js - Error Handling + +Tests error handling and edge cases: +- GET non-existent user (verify empty object response) +- GET non-existent post (verify graceful handling) +- Invalid endpoint (verify 404 with HTML) +- POST with minimal data (verify API resilience) + +**Qase features demonstrated:** `qase.id`, `qase.fields`, `qase.comment`, `qase.attach`, `qase.step` + +### api-advanced.test.js - Advanced Features + +Tests demonstrating advanced Qase features: +- Complex nested steps (user and posts retrieval) +- Suite hierarchy with tab separator +- Parameterized test pattern (multiple user IDs) +- Albums with nested photos +- Ignored test placeholder (future authentication feature) + +**Qase features demonstrated:** `qase.suite`, nested `qase.step`, `qase.parameters`, `qase.ignore`, `qase.comment` + +## Qase Features Demonstrated + +All 9 Qase reporter features are demonstrated across the test files: + +| Feature | API Method | Used In | Description | +|---------|-----------|---------|-------------| +| Test ID | `qase(id, name)` | All files | Links test to Qase test case using wrapper pattern | +| Title | `qase.title()` | (implicit via wrapper) | Test name is set via qase() wrapper | +| Fields | `qase.fields()` | All files | Sets severity, priority, layer metadata | +| Suite | `qase.suite()` | api-advanced.test.js | Organizes tests into hierarchical suites with `\t` separator | +| Steps | `await qase.step()` | All files | Defines execution steps with async/await support | +| Attachments | `qase.attach()` | api-crud, api-posts, api-errors | Attaches JSON content with `contentType` parameter | +| Comments | `qase.comment()` | api-crud, api-errors, api-advanced | Adds contextual notes to test results | +| Parameters | `qase.parameters()` | api-crud, api-posts, api-advanced | Reports parameterized test data | +| Ignore | `qase.ignore()` | api-advanced.test.js | Excludes specific test from Qase reporting | + +## Jest-Specific Patterns + +This example demonstrates Jest-specific Qase integration patterns: + +1. **Import Path:** Use `jest-qase-reporter/jest` (not the base package) + ```javascript + const { qase } = require('jest-qase-reporter/jest'); + ``` + +2. **Test ID Wrapper:** Use `qase(id, name)` wrapper pattern + ```javascript + test(qase(1, 'Test name'), async () => { + // test code + }); + ``` + +3. **Attachments:** Use `contentType` parameter (NOT `type`) + ```javascript + qase.attach({ + name: 'data.json', + content: JSON.stringify(data, null, 2), + contentType: 'application/json', + }); + ``` + +4. **Async Steps:** Use `await qase.step()` for async operations + ```javascript + await qase.step('Step name', async () => { + // async operations + }); + ``` + +5. **Suite Hierarchy:** Use `\t` (tab character) as separator + ```javascript + qase.suite('API Tests\tAdvanced\tRelationships'); + ``` + +## Project Structure + +``` +jest/ +├── tests/ +│ ├── api-crud.test.js # User CRUD operations +│ ├── api-posts.test.js # Post validation and filtering +│ ├── api-errors.test.js # Error handling scenarios +│ └── api-advanced.test.js # Advanced Qase features +├── jest.config.js # Jest configuration +├── qase.config.json # Qase reporter configuration +└── package.json +``` + +## About JSONPlaceholder + +[JSONPlaceholder](https://jsonplaceholder.typicode.com) is a free fake REST API for testing and prototyping. It provides: + +- **Free and stable:** No authentication required, always available +- **Realistic data:** 10 users, 100 posts, 500 comments, and more +- **Faked writes:** POST/PUT/DELETE operations are faked (return success but don't persist) +- **No rate limits:** Perfect for CI/CD pipelines + +### Available Endpoints + +- `/users` - 10 users with full profile information +- `/posts` - 100 posts associated with users +- `/comments` - 500 comments on posts +- `/albums` - 100 photo albums +- `/photos` - 5000 photos in albums +- `/todos` - 200 todo items + ## Additional Resources For more details on how to use this integration with Qase Test Management, visit diff --git a/examples/single/jest/jest.config.js b/examples/single/jest/jest.config.js index 522918ba..3030a779 100644 --- a/examples/single/jest/jest.config.js +++ b/examples/single/jest/jest.config.js @@ -1,5 +1,6 @@ module.exports = { globals: {}, + testTimeout: 10000, transform: { '^.+\\.(js|jsx)$': 'babel-jest', }, diff --git a/examples/single/jest/package.json b/examples/single/jest/package.json index 853639c1..02b3f0ba 100644 --- a/examples/single/jest/package.json +++ b/examples/single/jest/package.json @@ -2,7 +2,7 @@ "name": "examples-jest", "private": true, "scripts": { - "test": "QASE_MODE=testops jest --runInBand" + "test": "QASE_MODE=${QASE_MODE:-off} jest --runInBand" }, "devDependencies": { "@babel/preset-env": "^7.28.5", diff --git a/examples/single/jest/test/api-advanced.test.js b/examples/single/jest/test/api-advanced.test.js new file mode 100644 index 00000000..ff0ea973 --- /dev/null +++ b/examples/single/jest/test/api-advanced.test.js @@ -0,0 +1,130 @@ +const { qase } = require('jest-qase-reporter/jest'); +const { expect } = require('@jest/globals'); + +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +describe('JSONPlaceholder API - Advanced Features', () => { + test(qase(12, 'Complex nested steps - fetch user and their posts'), async () => { + qase.fields({ layer: 'api', severity: 'normal', priority: 'medium' }); + qase.suite('API Tests\tAdvanced\tRelationships'); + + let userId; + let userName; + + await qase.step('Fetch user details', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + expect(response.status).toBe(200); + + const user = await response.json(); + userId = user.id; + userName = user.name; + expect(userName).toBe('Leanne Graham'); + + await qase.step('Validate user has company information', async () => { + expect(user.company).toHaveProperty('name'); + expect(user.company.name).toBeTruthy(); + }); + }); + + await qase.step('Fetch all posts by user', async () => { + const response = await fetch(`${BASE_URL}/posts?userId=${userId}`); + expect(response.status).toBe(200); + + const posts = await response.json(); + expect(posts.length).toBe(10); + + await qase.step('Verify first post belongs to user', async () => { + expect(posts[0].userId).toBe(userId); + }); + + await qase.step('Verify post titles are non-empty', async () => { + posts.forEach((post) => { + expect(post.title).toBeTruthy(); + expect(post.body).toBeTruthy(); + }); + }); + }); + }); + + test(qase(13, 'Suite hierarchy demonstration'), async () => { + qase.suite('API Tests\tAdvanced\tData Validation'); + qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step('Validate todos endpoint structure', async () => { + const response = await fetch(`${BASE_URL}/todos/1`); + expect(response.status).toBe(200); + + const todo = await response.json(); + expect(todo).toHaveProperty('id'); + expect(todo).toHaveProperty('userId'); + expect(todo).toHaveProperty('title'); + expect(todo).toHaveProperty('completed'); + expect(typeof todo.completed).toBe('boolean'); + }); + }); + + test(qase(14, 'Parameterized test pattern - multiple user IDs'), async () => { + qase.parameters({ + testScope: 'multiple_users', + userIds: '1,2,3', + validationType: 'existence', + }); + qase.fields({ layer: 'api', severity: 'normal' }); + + const userIds = [1, 2, 3]; + + for (const userId of userIds) { + await qase.step(`Verify user ${userId} exists`, async () => { + const response = await fetch(`${BASE_URL}/users/${userId}`); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user.id).toBe(userId); + expect(user.name).toBeTruthy(); + }); + } + }); + + test(qase(15, 'Albums endpoint with nested photos'), async () => { + qase.fields({ layer: 'api', severity: 'low', priority: 'low' }); + + let albumId; + + await qase.step('Fetch album details', async () => { + const response = await fetch(`${BASE_URL}/albums/1`); + expect(response.status).toBe(200); + + const album = await response.json(); + albumId = album.id; + expect(albumId).toBe(1); + expect(album.userId).toBe(1); + }); + + await qase.step('Fetch photos in album', async () => { + const response = await fetch(`${BASE_URL}/albums/${albumId}/photos`); + expect(response.status).toBe(200); + + const photos = await response.json(); + expect(Array.isArray(photos)).toBe(true); + expect(photos.length).toBe(50); + + await qase.step('Verify photo structure', async () => { + const firstPhoto = photos[0]; + expect(firstPhoto).toHaveProperty('albumId'); + expect(firstPhoto).toHaveProperty('id'); + expect(firstPhoto).toHaveProperty('title'); + expect(firstPhoto).toHaveProperty('url'); + expect(firstPhoto).toHaveProperty('thumbnailUrl'); + }); + }); + }); + + test.skip(qase(16, 'Authentication feature (not yet implemented)'), async () => { + qase.ignore(); + qase.comment('This test will be implemented when JSONPlaceholder adds authentication support'); + qase.fields({ layer: 'api', severity: 'high', priority: 'high' }); + + // Placeholder for future authentication tests + expect(true).toBe(true); + }); +}); diff --git a/examples/single/jest/test/api-crud.test.js b/examples/single/jest/test/api-crud.test.js new file mode 100644 index 00000000..e2cfd9a8 --- /dev/null +++ b/examples/single/jest/test/api-crud.test.js @@ -0,0 +1,107 @@ +const { qase } = require('jest-qase-reporter/jest'); +const { expect } = require('@jest/globals'); + +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +describe('JSONPlaceholder API - User CRUD Operations', () => { + test(qase(1, 'GET all users returns 10 users'), async () => { + qase.fields({ layer: 'api', severity: 'normal', priority: 'high' }); + + await qase.step('Send GET request to /users endpoint', async () => { + const response = await fetch(`${BASE_URL}/users`); + expect(response.status).toBe(200); + + const users = await response.json(); + expect(Array.isArray(users)).toBe(true); + expect(users.length).toBe(10); + }); + + await qase.step('Verify response contains valid user structure', async () => { + const response = await fetch(`${BASE_URL}/users`); + const users = await response.json(); + const firstUser = users[0]; + + expect(firstUser).toHaveProperty('id'); + expect(firstUser).toHaveProperty('name'); + expect(firstUser).toHaveProperty('email'); + expect(firstUser).toHaveProperty('address'); + }); + }); + + test(qase(2, 'GET single user by ID returns correct user'), async () => { + qase.parameters({ userId: 1 }); + qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step('Send GET request to /users/1', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user.id).toBe(1); + expect(user.name).toBe('Leanne Graham'); + expect(user.email).toBe('Sincere@april.biz'); + }); + + await qase.step('Verify user address and company information', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + const user = await response.json(); + + expect(user.address).toHaveProperty('city'); + expect(user.address.city).toBe('Gwenborough'); + expect(user.company).toHaveProperty('name'); + }); + }); + + test(qase(3, 'POST create new user returns 201 with ID'), async () => { + qase.fields({ layer: 'api', severity: 'critical', priority: 'high' }); + + const newUser = { + name: 'Test User', + username: 'testuser', + email: 'test@example.com', + }; + + await qase.step('Send POST request to create user', async () => { + const response = await fetch(`${BASE_URL}/users`, { + method: 'POST', + body: JSON.stringify(newUser), + headers: { 'Content-Type': 'application/json' }, + }); + + expect(response.status).toBe(201); + + const createdUser = await response.json(); + expect(createdUser).toHaveProperty('id'); + expect(createdUser.id).toBe(11); + + // Attach the request body + qase.attach({ + name: 'request-body.json', + content: JSON.stringify(newUser, null, 2), + contentType: 'application/json', + }); + }); + }); + + test(qase(4, 'DELETE user returns 200 status'), async () => { + qase.fields({ layer: 'api', severity: 'normal' }); + qase.comment('Note: JSONPlaceholder fakes DELETE operations - no actual deletion occurs'); + + await qase.step('Send DELETE request to /users/1', async () => { + const response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + + expect(response.status).toBe(200); + }); + + await qase.step('Verify response is empty object', async () => { + const response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE', + }); + + const result = await response.json(); + expect(result).toEqual({}); + }); + }); +}); diff --git a/examples/single/jest/test/api-errors.test.js b/examples/single/jest/test/api-errors.test.js new file mode 100644 index 00000000..44627e3b --- /dev/null +++ b/examples/single/jest/test/api-errors.test.js @@ -0,0 +1,74 @@ +const { qase } = require('jest-qase-reporter/jest'); +const { expect } = require('@jest/globals'); + +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +describe('JSONPlaceholder API - Error Handling', () => { + test(qase(8, 'GET non-existent user returns empty object'), async () => { + qase.fields({ layer: 'api', severity: 'normal', priority: 'medium' }); + qase.comment('JSONPlaceholder returns empty object {} for non-existent resources instead of 404'); + + await qase.step('Request user with ID 999', async () => { + const response = await fetch(`${BASE_URL}/users/999`); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user).toEqual({}); + }); + }); + + test(qase(9, 'GET non-existent post returns empty object'), async () => { + qase.fields({ layer: 'api', severity: 'normal' }); + qase.comment('JSONPlaceholder behavior: non-existent resources return {} with 200 status'); + + await qase.step('Request post with ID 999', async () => { + const response = await fetch(`${BASE_URL}/posts/999`); + expect(response.status).toBe(200); + + const post = await response.json(); + expect(post).toEqual({}); + + // Attach the error response + qase.attach({ + name: 'error-response.json', + content: JSON.stringify({ + requestedId: 999, + response: post, + status: response.status, + }, null, 2), + contentType: 'application/json', + }); + }); + }); + + test(qase(10, 'Invalid endpoint returns 404 with HTML'), async () => { + qase.fields({ layer: 'api', severity: 'low', priority: 'low' }); + qase.comment('Invalid endpoints return 404 with HTML error page'); + + await qase.step('Request invalid endpoint', async () => { + const response = await fetch(`${BASE_URL}/invalid-endpoint`); + expect(response.status).toBe(404); + + const contentType = response.headers.get('content-type'); + expect(contentType).toContain('text/html'); + }); + }); + + test(qase(11, 'POST with invalid JSON is gracefully handled'), async () => { + qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step('Send POST with minimal data', async () => { + const response = await fetch(`${BASE_URL}/posts`, { + method: 'POST', + body: JSON.stringify({}), + headers: { 'Content-Type': 'application/json' }, + }); + + // JSONPlaceholder accepts any JSON and returns 201 + expect(response.status).toBe(201); + + const result = await response.json(); + expect(result).toHaveProperty('id'); + }); + }); +}); diff --git a/examples/single/jest/test/api-posts.test.js b/examples/single/jest/test/api-posts.test.js new file mode 100644 index 00000000..bfdd29be --- /dev/null +++ b/examples/single/jest/test/api-posts.test.js @@ -0,0 +1,97 @@ +const { qase } = require('jest-qase-reporter/jest'); +const { expect } = require('@jest/globals'); + +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +describe('JSONPlaceholder API - Post Validation', () => { + test(qase(5, 'GET all posts returns 100 posts'), async () => { + qase.fields({ layer: 'api', severity: 'normal', priority: 'high' }); + + await qase.step('Send GET request to /posts endpoint', async () => { + const response = await fetch(`${BASE_URL}/posts`); + expect(response.status).toBe(200); + + const posts = await response.json(); + expect(Array.isArray(posts)).toBe(true); + expect(posts.length).toBe(100); + }); + + await qase.step('Verify post structure', async () => { + const response = await fetch(`${BASE_URL}/posts`); + const posts = await response.json(); + const firstPost = posts[0]; + + expect(firstPost).toHaveProperty('id'); + expect(firstPost).toHaveProperty('userId'); + expect(firstPost).toHaveProperty('title'); + expect(firstPost).toHaveProperty('body'); + }); + }); + + test(qase(6, 'GET posts filtered by user ID returns correct results'), async () => { + qase.parameters({ userId: 1, filterType: 'query_parameter' }); + qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step('Send GET request with userId filter', async () => { + const response = await fetch(`${BASE_URL}/posts?userId=1`); + expect(response.status).toBe(200); + + const posts = await response.json(); + expect(Array.isArray(posts)).toBe(true); + expect(posts.length).toBe(10); + }); + + await qase.step('Verify all posts belong to user 1', async () => { + const response = await fetch(`${BASE_URL}/posts?userId=1`); + const posts = await response.json(); + + posts.forEach((post) => { + expect(post.userId).toBe(1); + }); + }); + }); + + test(qase(7, 'GET post with comments returns nested data'), async () => { + qase.fields({ layer: 'api', severity: 'normal', priority: 'medium' }); + + let postData; + + await qase.step('Fetch post by ID', async () => { + const response = await fetch(`${BASE_URL}/posts/1`); + expect(response.status).toBe(200); + + postData = await response.json(); + expect(postData.id).toBe(1); + expect(postData.title).toBeTruthy(); + }); + + await qase.step('Fetch comments for the post', async () => { + const response = await fetch(`${BASE_URL}/posts/1/comments`); + expect(response.status).toBe(200); + + const comments = await response.json(); + expect(Array.isArray(comments)).toBe(true); + expect(comments.length).toBe(5); + + // Attach the full response with post and comments + qase.attach({ + name: 'post-with-comments.json', + content: JSON.stringify({ post: postData, comments }, null, 2), + contentType: 'application/json', + }); + }); + + await qase.step('Verify comment structure', async () => { + const response = await fetch(`${BASE_URL}/posts/1/comments`); + const comments = await response.json(); + const firstComment = comments[0]; + + expect(firstComment).toHaveProperty('postId'); + expect(firstComment).toHaveProperty('id'); + expect(firstComment).toHaveProperty('name'); + expect(firstComment).toHaveProperty('email'); + expect(firstComment).toHaveProperty('body'); + expect(firstComment.postId).toBe(1); + }); + }); +}); diff --git a/examples/single/jest/test/attach.test.js b/examples/single/jest/test/attach.test.js deleted file mode 100644 index f091c0c8..00000000 --- a/examples/single/jest/test/attach.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: attach.test.js", () => { - test("Test result with attachment", async () => { - - // To attach a single file - qase.attach({ - paths: ["./test/attachments/test-file.txt"], - }); - - /* - // Add multiple attachments. - await qase.attach({ paths: ['/path/to/file', '/path/to/another/file'] }); - - */ - // Upload file's contents directly from code. - qase.attach({ - name: "attachment.txt", - content: "Hello, world!", - contentType: "text/plain", - }); - - expect(true).toBe(true); - }); -}); diff --git a/examples/single/jest/test/attachments/test-file.txt b/examples/single/jest/test/attachments/test-file.txt deleted file mode 100644 index bcf81250..00000000 Binary files a/examples/single/jest/test/attachments/test-file.txt and /dev/null differ diff --git a/examples/single/jest/test/comment.test.js b/examples/single/jest/test/comment.test.js deleted file mode 100644 index e4fb2e7c..00000000 --- a/examples/single/jest/test/comment.test.js +++ /dev/null @@ -1,16 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: comment.test.js", () => { - test("A test case with qase.comment()", () => { - /* - * Please note, this comment is added to a Result, not to the Test case. - */ - - qase.comment( - "This comment will be displayed in the 'Actual Result' field of the test result in Qase.", - ); - - expect(true).toBe(true); - }); -}); diff --git a/examples/single/jest/test/fields.test.js b/examples/single/jest/test/fields.test.js deleted file mode 100644 index 02ecdaff..00000000 --- a/examples/single/jest/test/fields.test.js +++ /dev/null @@ -1,91 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const { markdownContent } = require("./markdownContent"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: fields.test.js\tTest cases with field: Priority", () => { - /* - * Meta data such as Priority, Severity, Layer fields, Description, and pre-conditions can be updated from code. - * This enables you to manage test cases from code directly. - */ - - test("Priority = low", async () => { - await qase.fields({ priority: "low" }); - expect(true).toBe(true); - }); - - test("Priority = medium", async () => { - await qase.fields({ priority: "medium" }); - expect(true).toBe(true); - }); - - test("Priority = high", async () => { - await qase.fields({ priority: "high" }); - expect(true).toBe(true); - }); -}); - -describe("Example: fields.test.js\tTest cases with field: Severity", () => { - test("Severity = trivial", async () => { - await qase.fields({ severity: "trivial" }); - expect(true).toBe(true); - }); - - test("Severity = minor", async () => { - await qase.fields({ severity: "minor" }); - expect(true).toBe(true); - }); - - test("Severity = normal", async () => { - await qase.fields({ severity: "normal" }); - expect(true).toBe(true); - }); - - test("Severity = major", async () => { - await qase.fields({ severity: "major" }); - expect(true).toBe(true); - }); - - test("Severity = critical", async () => { - await qase.fields({ severity: "critical" }); - expect(true).toBe(true); - }); - - test("Severity = blocker", async () => { - await qase.fields({ severity: "blocker" }); - expect(true).toBe(true); - }); -}); - -describe("Example: fields.test.js\tTest cases with field: Layer", () => { - test("Layer = e2e", async () => { - await qase.fields({ layer: "e2e" }); - expect(true).toBe(true); - }); - - test("Layer = api", async () => { - await qase.fields({ layer: "api" }); - expect(true).toBe(true); - }); - - test("Layer = unit", async () => { - await qase.fields({ layer: "unit" }); - expect(true).toBe(true); - }); -}); - -describe("Example: fields.test.js\tTest cases with Description, Pre & Post Conditions", () => { - test("Description with Markdown Support", async () => { - await qase.fields({ description: markdownContent }); - expect(true).toBe(true); - }); - - test("Preconditions with Markdown Support", async () => { - await qase.fields({ preconditions: markdownContent }); - expect(true).toBe(true); - }); - - test("Postconditions with Markdown Support", async () => { - await qase.fields({ postconditions: markdownContent }); - expect(true).toBe(true); - }); -}); diff --git a/examples/single/jest/test/id.test.js b/examples/single/jest/test/id.test.js deleted file mode 100644 index 82b47cf4..00000000 --- a/examples/single/jest/test/id.test.js +++ /dev/null @@ -1,9 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: id.test.js", () => { - // Please, change the Id from `1` to any case Id present in your project before uncommenting the test. - test(qase(1, "A test with Qase Id"), () => { - expect(true).toBe(true); - }); -}); diff --git a/examples/single/jest/test/ignore.test.js b/examples/single/jest/test/ignore.test.js deleted file mode 100644 index a9c7f7ed..00000000 --- a/examples/single/jest/test/ignore.test.js +++ /dev/null @@ -1,9 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: ignore.test.js", () => { - test("This test is executed using Jest; however, it is NOT reported to Qase", () => { - qase.ignore(); - expect(true).toBe(true); - }); -}); diff --git a/examples/single/jest/test/markdownContent.js b/examples/single/jest/test/markdownContent.js deleted file mode 100644 index 3f3583c6..00000000 --- a/examples/single/jest/test/markdownContent.js +++ /dev/null @@ -1,111 +0,0 @@ -export const markdownContent = `# Markdown Syntax Showcase - -## Headers -### Different Header Levels -#### Are Supported -##### In Markdown -###### Even Smallest Headers - -
- -## Text Formatting -*Italic Text* -**Bold Text** -***Bold and Italic*** -~~Strikethrough Text~~ - -
- -## Lists -### Unordered Lists -- First item -- Second item - * Nested item - * Another nested item - -
- -### Ordered Lists -1. First ordered item -2. Second ordered item - 1. Nested ordered item - 2. Another nested ordered item - -
- -# # Links -[Inline Link](https://www.example.com) -[Link with Title](https://www.example.com "Website Title") - -[Reference-style Link][Reference] -[Reference]: https://www.example.com - -
- -## Code -### Inline Code -Here is some \`inline code\` - -### Code Blocks -\`\`\`javascript -function exampleCode() { - return "Code blocks are supported"; -} -\`\`\` - -\`\`\`python -def python_example(): - return "Multiple language syntax highlighting" -\`\`\` - -
- -## Blockquotes -> This is a blockquote -> -> It can span multiple lines -> -> ### Even with Headers Inside -> -> - And lists -> - Are possible - -
- -## Horizontal Rules ---- -*** -___ - -
- -## Tables -| Column 1 | Column 2 | Column 3 | -|----------|----------|----------| -| Row 1, Col 1 | Row 1, Col 2 | Row 1, Col 3 | -| Row 2, Col 1 | Row 2, Col 2 | Row 2, Col 3 | - -
- -## Task Lists -- [x] Completed task -- [ ] Incomplete task -- [ ] Another incomplete task - -
- -## Footnotes -Here's a sentence with a footnote[^1]. - -[^1]: This is the footnote content. - -
- -## HTML Inline Elements -Some underlined and superscript text. - -
- -## Escaping Characters -\\*This is not italicized\\* -\\# This is a literal hash`; diff --git a/examples/single/jest/test/params.test.js b/examples/single/jest/test/params.test.js deleted file mode 100644 index 71d7f741..00000000 --- a/examples/single/jest/test/params.test.js +++ /dev/null @@ -1,44 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -const testCases = [ - { browser: "Chromium", username: "@alice", password: "123" }, - { browser: "Firefox", username: "@bob", password: "456" }, - { browser: "Webkit", username: "@charlie", password: "789" }, -]; - -describe("Example param.test.js\tSingle Parameter", () => { - testCases.forEach(({ browser }) => { - test(`Test login with ${browser}`, () => { - qase.title("Verify if login page loads successfully"); - - /* - * Instead of creating three separate test cases in Qase, this method will add a 'browser' parameter, with three values. - */ - - qase.parameters({ Browser: browser }); - - expect(true).toBe(true); - }); - }); -}); - -describe("Example param.test.js\tGroup Parameter", () => { - testCases.forEach(({ username, password }) => { - test(`Test login with ${username} using qase.groupParameters`, () => { - qase.title("Verify if user is able to login with their username."); - - /* - * Here, we're grouping the username and password parameters to track them together, as a set of parameters for the test. - * This will show the username and password combinations for the test. - */ - - qase.groupParameters({ - Username: username, - Password: password, - }); - - expect(true).toBe(true); - }); - }); -}); diff --git a/examples/single/jest/test/steps.test.js b/examples/single/jest/test/steps.test.js deleted file mode 100644 index e7b0e4f2..00000000 --- a/examples/single/jest/test/steps.test.js +++ /dev/null @@ -1,40 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: steps.test.js", () => { - test("A Test case with steps, updated from code", async () => { - await qase.step("Initialize the environment", async () => { - // Set up test environment - }); - - await qase.step("Test Core Functionality of the app", async () => { - // Exercise core functionality - }); - - await qase.step("Verify Expected Behavior of the app", async () => { - // Assert expected behavior - }); - - await qase.step( - "Verify if user is able to log out successfully", - async () => { - // Expected user to be logged out (but, ran into a problem!). - expect(true).toBe(true); - }, - ); - }); - - test("A Test case with steps including expected results and data", async () => { - await qase.step("Click button", async () => { - // Click action - }, "Button should be clicked", "Button data"); - - await qase.step("Fill form", async () => { - // Form filling action - }, "Form should be filled", "Form input data"); - - await qase.step("Submit form", async () => { - // Submit action - }, "Form should be submitted", "Form submission data"); - }); -}); diff --git a/examples/single/jest/test/suite.test.js b/examples/single/jest/test/suite.test.js deleted file mode 100644 index 95420f2c..00000000 --- a/examples/single/jest/test/suite.test.js +++ /dev/null @@ -1,17 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: suite.test.js", () => { - test("Test with a defined suite", () => { - qase.suite("Example: suite.test.js\tThis shall be a suite name"); - expect(true).toBe(true); - }); - - test("Test within multiple levels of suite", () => { - qase.suite( - "Example: suite.test.js\tThis shall be a suite name\tChild Suite", - ); - // A `\t` is used for dividing each suite name - expect(true).toBe(true); - }); -}); diff --git a/examples/single/jest/test/title.test.js b/examples/single/jest/test/title.test.js deleted file mode 100644 index c57426ff..00000000 --- a/examples/single/jest/test/title.test.js +++ /dev/null @@ -1,43 +0,0 @@ -const { qase } = require("jest-qase-reporter/jest"); -const {describe, test, expect} = require("@jest/globals"); - -describe("Example: title.test.js", () => { - test("Test without qase.title() method", () => { - /* - * Here, we're are not using a qase.title() method - * Given, you have "Auto-create cases" option enabled for this project. - * A new test will be created in Qase, with the test's title. - */ - - expect(true).toBe(true); - }); - - test("This won't appear in Qase", () => { - qase.title("This text will be the title of the test, in Qase"); - - /* - * Here, the Qase Test case's title will be taken from qase.title() method. - */ - - expect(true).toBe(true); - }); -}); - -/* - * - * Q) What about the tests where the qase.title() method is not used? - * => Those test cases will have the "Title of this test" as the newly created case's title. - * - * - * Q) I'm running this test case, but it's not creating any test case in Qase. - * My test run is empty, what am I doing wrong? - * - * => Go to your Qase Project's settings, switch to the Test runs tab. - * Under "Automated Testing" - Enable "Create test cases option" [https://i.imgur.com/PtZPrrY.png] - * - * - * Q) What happens if I change the title in `qase.title()` ? - * => Since, there's no link between the Qase test case and this test, changing the title will lead to - * a new case being created in your Project repository. - * - */ diff --git a/examples/single/mocha/.mocharc.js b/examples/single/mocha/.mocharc.js index c6db4415..dd137c32 100644 --- a/examples/single/mocha/.mocharc.js +++ b/examples/single/mocha/.mocharc.js @@ -1,4 +1,5 @@ module.exports = { - spec: ["test/*.spec.js"], - reporter: "mocha-qase-reporter" + spec: ["test/**/*.spec.js"], + reporter: "mocha-qase-reporter", + timeout: 10000 }; diff --git a/examples/single/mocha/README.md b/examples/single/mocha/README.md index 7eab23d1..53859de0 100644 --- a/examples/single/mocha/README.md +++ b/examples/single/mocha/README.md @@ -1,352 +1,240 @@ -# Mocha Example +# Mocha Example - API Testing with Qase Integration -This is a sample project demonstrating how to write and execute tests using the Mocha framework with integration to -Qase Test Management. +## Overview -## Prerequisites - -Ensure that the following tools are installed on your machine: - -1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) -2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) - -## Setup Instructions - -1. Clone this repository by running the following commands: - - ```bash - git clone https://github.com/qase-tms/qase-javascript.git - cd qase-javascript/examples/mocha - ``` - -2. Install the project dependencies: +This example demonstrates realistic API testing scenarios using Mocha with Qase TestOps integration. The tests validate a public REST API (JSONPlaceholder) while showcasing all Qase reporter features in practical contexts, including CRUD operations on users, post validation and filtering, error handling for 404 responses, and advanced features like nested steps, suite hierarchies, and parameterized tests. - ```bash - npm install - ``` - -3. Create a `qase.config.json` file in the root of the project. Follow the instructions - on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). - -4. To run tests and upload the results to Qase Test Management, use the following command: - - ```bash - npm run test - ``` - -## Features Demonstrated - -- **Qase Integration**: Shows various Qase features like test IDs, titles, suites, comments, fields, attachments, and steps -- **Test Methods**: Demonstrates different ways to use Qase methods in tests -- **Async Tests**: Shows how to handle asynchronous test execution -- **Parametrized Tests**: Demonstrates parameterized test execution -- **Attachment Handling**: Shows how to attach files and data to tests +## Prerequisites -## Usage +- Node.js 18 or higher (for native `fetch` support) +- npm or yarn -### Basic Test Execution +## Installation ```bash -npm run test +npm install ``` -This runs all tests with the Qase reporter, sending results to Qase TestOps. +## Configuration -### Alternative: Direct Mocha Command +The Qase reporter can be configured using environment variables or configuration files. -```bash -QASE_MODE=testops mocha -``` - -This runs Mocha directly with Qase integration. +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) -## Configuration Files - -### qase.config.json +Create a `qase.config.json` file in the root directory: ```json { "debug": true, "testops": { "api": { - "token": "your_qase_token_here" + "token": "your_qase_api_token" }, - "project": "your_project_code_here", + "project": "your_project_code", "uploadAttachments": true, - "showPublicReportLink": true, "run": { "complete": true, - "title": "Mocha test run" + "title": "Mocha API Test Run" } } } ``` -**Important**: Replace `your_qase_token_here` and `your_project_code_here` with your actual Qase token and project code for TestOps mode to work. - -### Getting Your Qase Credentials - -1. **API Token**: - - Go to your Qase profile settings - - Navigate to "API Tokens" section - - Create a new token or copy an existing one - -2. **Project Code**: - - Go to your Qase project - - The project code is shown in the URL: `https://app.qase.io/project/YOUR_PROJECT_CODE` - - Or find it in project settings - -## Expected Behavior - -### Running with QASE_MODE=off (Local Development) - -When running tests with `QASE_MODE=off`, tests execute normally without Qase reporting: - -- Tests run and pass/fail as usual -- No data is sent to Qase TestOps -- No Qase API token required -- Output shows standard Mocha test results - -This mode is useful for local development and debugging. +**Getting your credentials:** +- **API Token**: Qase profile settings → API Tokens section +- **Project Code**: Found in your Qase project URL or settings -### Running with QASE_MODE=testops (CI/CD and Reporting) +## Running Tests -When running tests with `QASE_MODE=testops`, test results are reported to Qase: - -- Tests execute and results are sent to Qase TestOps -- A new test run is created in your Qase project -- Test results include all metadata (steps, attachments, fields, etc.) -- Console output includes Qase test run link -- Requires valid Qase API token and project code in `qase.config.json` +```bash +# Run tests without Qase reporting (default) +npm test -**Steps Example:** -- Creates test result with multiple named steps using `this.step()` -- Each step shows execution status, duration, and any errors -- Nested steps appear hierarchically in Qase -- Steps with callbacks can contain assertions and actions +# Run tests with Qase reporting +QASE_MODE=testops npm test +``` -**Attachments Example:** -- Files attached via `this.attach()` appear in test results -- Content can be strings, buffers, or file paths -- Attachments are visible in the test run details -- Supports text, JSON, images, and binary files +## Test Scenarios -**Context Methods:** -- Mocha provides Qase methods via test context (`this` keyword) -- Must use `function()` syntax (not arrow functions) to access `this` -- Available methods: `this.title()`, `this.suite()`, `this.fields()`, `this.comment()`, `this.parameters()`, `this.attach()`, `this.step()` +### 1. `api-crud.spec.js` - User CRUD Operations +Tests basic CRUD operations on the `/users` endpoint: +- **GET all users** - Verifies 10 users returned with proper structure +- **GET single user** - Validates specific user details (Leanne Graham) +- **POST create user** - Tests user creation with request/response attachments +- **DELETE user** - Verifies deletion (faked by JSONPlaceholder) -## What You'll See +**Qase features:** `qase.id`, `qase.fields`, `qase.step`, `qase.attach`, `qase.comment`, `qase.parameters` -### Test Execution Output +### 2. `api-posts.spec.js` - Post Validation +Tests post retrieval and filtering: +- **GET all posts** - Verifies 100 posts with correct structure +- **GET posts by user** - Tests filtering with query parameters +- **GET post with comments** - Validates relationships and nested data -When running tests with Qase integration, you'll see: +**Qase features:** `qase.id`, `qase.fields`, `qase.parameters`, `qase.step`, `qase.attach` -1. **Qase Integration**: DEBUG logs showing data being sent to Qase TestOps -2. **Test Run Creation**: Results are sent to Qase TestOps -3. **Test Run Link**: URL to view results in Qase dashboard -4. **Test Results**: Individual test results with Qase IDs +### 3. `api-errors.spec.js` - Error Handling +Tests error scenarios and 404 responses: +- **Non-existent user** - Validates 404 handling for invalid user ID +- **Non-existent post** - Attaches error response for documentation +- **Invalid endpoint** - Verifies graceful handling of bad URLs -Example output: +**Qase features:** `qase.id`, `qase.comment`, `qase.step`, `qase.attach` -``` -[DEBUG] qase: Starting test run -[DEBUG] qase: Creating test run: {"title":"Mocha test run","description":"","is_autotest":true,"cases":[],"start_time":"2025-10-07 19:30:00","tags":[]} -[DEBUG] qase: Test run created: {"status":true,"result":{"id":3743}} - - Simple tests async - ✔ test without qase metadata success - 1) test without qase metadata failed - - test with qase id success (Qase ID: 10) - 2) test with qase id failed (Qase ID: 20) - ✔ test with title success - 3) test with title failed - ✔ test with suite success - 4) test with suite failed - ✔ test with comment success - 5) test with comment failed - ✔ test with fields success - 6) test with fields failed - ✔ ignored test success - 7) ignored test failed - - Attachment tests - ✔ successful test with string attachment - 8) failing test with string attachment - - Parametrized test - ✔ test with parameters success 1 - 9) test with parameters failed 1 - ✔ test with parameters success 2 - 10) test with parameters failed 2 - ✔ test with parameters success 3 - 11) test with parameters failed 3 - ✔ test with parameters success 4 - 12) test with parameters failed 4 - ✔ test with parameters success 5 - 13) test with parameters failed 5 - - Simple tests - ✔ test without qase metadata success - 14) test without qase metadata failed - - test with qase id success (Qase ID: 1) - 15) test with qase id failed (Qase ID: 2) - ✔ test with title success - 16) test with title failed - ✔ test with suite success - 17) test with suite failed - ✔ test with comment success - 18) test with comment failed - ✔ test with fields success - 19) test with fields failed - ✔ ignored test success - 20) ignored test failed - - Step tests - ✔ successful test with steps - 21) failing test with steps - - 2 passing (11ms) - 2 pending - 38 failing - -[DEBUG] qase: Publishing test run results -[DEBUG] qase: Results sent to Qase: 40 -[INFO] qase: Test run link: https://app.qase.io/run/DEVX/dashboard/3743 -[INFO] qase: Run 3743 completed -``` +### 4. `api-advanced.spec.js` - Advanced Features +Demonstrates complex Qase capabilities: +- **Complex nested steps** - Multi-level step hierarchy for related API calls +- **Suite hierarchy** - Nested suite structure with `\t` separator +- **Parameterized patterns** - Testing multiple resources with parameters +- **Ignored test** - Placeholder for future authentication features -## Benefits +**Qase features:** `qase.id`, `qase.fields`, `qase.suite`, `qase.step`, `qase.parameters`, `qase.attach`, `qase.comment`, `qase.ignore` -This example demonstrates comprehensive Qase integration with Mocha: +## Qase Features Demonstrated -- **Full Qase Functionality**: All Qase methods (`this.title()`, `this.suite()`, etc.) are available -- **TestOps Integration**: Results are sent to Qase TestOps for analysis -- **Rich Test Metadata**: Support for titles, suites, comments, fields, attachments, and steps -- **Async Support**: Proper handling of asynchronous test execution -- **Parametrized Tests**: Support for parameterized test execution -- **Attachment Support**: Ability to attach files and data to tests -- **CI/CD Ready**: Perfect for automated testing pipelines +All 9 Qase reporter features demonstrated in these tests: -## Test Examples +| Feature | Usage | Example File | +|---------|-------|--------------| +| **Test ID** | `qase(id, 'name')` wrapper | All files | +| **Title** | `qase.title('custom title')` | Not needed (using qase wrapper) | +| **Fields** | `qase.fields({ layer, severity, priority })` | api-crud, api-posts, api-advanced | +| **Suite** | `qase.suite('Parent\tChild\tGrandchild')` | api-advanced | +| **Steps** | `await qase.step('name', async () => {})` | All files | +| **Attachments** | `qase.attach({ name, content, contentType })` | api-crud, api-posts, api-errors, api-advanced | +| **Comments** | `qase.comment('additional info')` | api-crud, api-errors, api-advanced | +| **Parameters** | `qase.parameters({ key: 'value' })` | api-crud, api-posts, api-advanced | +| **Ignore** | `qase.ignore()` with `it.skip()` | api-advanced | -### Basic Test with Qase ID +## Mocha-Specific Patterns +### Import from Correct Path ```javascript const { qase } = require('mocha-qase-reporter/mocha'); - -it(qase(1, 'test with qase id'), function() { - assert.strictEqual(1, 1); -}); ``` +**Important:** Must import from `mocha-qase-reporter/mocha` (not base package). -### Test with Custom Title - +### Test ID Wrapper Pattern ```javascript -it('test with title', function() { - this.title('Custom Test Title'); - assert.strictEqual(1, 1); +it(qase(1, 'Test description'), async function() { + // Test code }); ``` +The `qase(id, name)` wrapper assigns Qase test case IDs. -### Test with Suite - +### Attachment contentType Parameter ```javascript -it('test with suite', function() { - this.suite('Custom Suite'); - assert.strictEqual(1, 1); +qase.attach({ + name: 'data.json', + content: JSON.stringify(data), + contentType: 'application/json' // Note: contentType, not type }); ``` +**Important:** Mocha uses `contentType` parameter (Vitest uses `type`). -### Test with Comment - +### Steps Can Be Sync or Async ```javascript -it('test with comment', function() { - this.comment('This is a test comment'); - assert.strictEqual(1, 1); +// Async steps (recommended for API calls) +await qase.step('Fetch data', async () => { + const response = await fetch(url); }); -``` - -### Test with Fields -```javascript -it('test with fields', function() { - this.fields({ environment: 'test', priority: 'high' }); - assert.strictEqual(1, 1); +// Sync steps (for simple operations) +qase.step('Validate data', () => { + assert.strictEqual(value, expected); }); ``` -### Test with Attachment - +### Suite Hierarchy with \t Separator ```javascript -it('test with attachment', function() { - this.attach({ - name: 'test-data.txt', - content: 'Sample test data', - contentType: 'text/plain' - }); - assert.strictEqual(1, 1); -}); +qase.suite('API Tests\tAdvanced\tRelationships'); +// Creates: API Tests > Advanced > Relationships in Qase UI ``` -### Test with Steps +### Function Context for `this` Access +While modern API uses the `qase` object directly, traditional Mocha patterns use `function()` syntax (not arrow functions) to access `this` context: ```javascript -it('test with steps', function() { - this.step('Step 1: Initialize', () => { - assert.strictEqual(1, 1); - }); - - this.step('Step 2: Execute', () => { - assert.strictEqual(2, 2); - }); - - this.step('Step 3: Verify', () => { - assert.strictEqual(3, 3); - }); +// Modern approach (used in these examples) +it('test', function() { + qase.title('Custom title'); }); -``` - -### Parametrized Test - -```javascript -const parameters = [1, 2, 3, 4, 5]; -parameters.forEach((param, index) => { - it(`test with parameters success ${param}`, function() { - this.parameters({ value: param }); - assert.strictEqual(param, param); - }); +// Traditional approach (also valid) +it('test', function() { + this.title('Custom title'); }); ``` -## Troubleshooting - -### TestOps Mode Failing +## Project Structure -If TestOps mode fails with "Uncaught error outside test suite", make sure you have: +``` +mocha/ +├── test/ +│ ├── api-crud.spec.js # User CRUD operations +│ ├── api-posts.spec.js # Post validation and filtering +│ ├── api-errors.spec.js # Error handling scenarios +│ └── api-advanced.spec.js # Advanced Qase features +├── .mocharc.js # Mocha configuration +├── qase.config.json # Qase reporter configuration +└── package.json +``` -1. **Valid Qase Token**: Replace `your_qase_token_here` in `qase.config.json` with your actual Qase API token -2. **Valid Project Code**: Replace `your_project_code_here` in `qase.config.json` with your actual project code -3. **Network Access**: Ensure you can reach Qase API from your environment +## JSONPlaceholder API -### Qase Methods Not Working +These tests use [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free fake REST API: -If Qase methods (`this.title()`, `this.suite()`, etc.) are not working: +**Key endpoints:** +- `/users` - 10 users +- `/posts` - 100 posts +- `/comments` - 500 comments +- `/albums` - 100 albums +- `/photos` - 5000 photos -1. **Check Reporter**: Make sure you're using `mocha-qase-reporter` -2. **Check Mode**: Ensure `QASE_MODE=testops` is set -3. **Check Configuration**: Verify `qase.config.json` is properly configured +**Important notes:** +- **No authentication required** - Perfect for testing examples +- **Write operations are faked** - POST/PUT/PATCH/DELETE return success but don't persist data +- **Stable and reliable** - Hosted by Vercel, widely used for testing +- **CORS enabled** - Can be used from browser or Node.js -### Async Tests Not Working +## Expected Output -If async tests are not working properly: +When running with `QASE_MODE=testops`, you'll see: -1. **Use Proper Async Syntax**: Use `async/await` or return promises -2. **Check Timeouts**: Ensure timeout is sufficient for async operations -3. **Handle Errors**: Properly handle and assert async errors +``` +JSONPlaceholder User CRUD Operations + ✓ GET all users - verify 10 users returned (Qase ID: 1) + ✓ GET single user by ID - verify user details (Qase ID: 2) + ✓ POST create user - verify 201 response and returned ID (Qase ID: 3) + ✓ DELETE user - verify 200 response (Qase ID: 4) + +JSONPlaceholder Post Validation + ✓ GET all posts - verify 100 posts returned (Qase ID: 5) + ✓ GET posts by user ID - verify filtered results (Qase ID: 6) + ✓ GET post with comments - verify comment structure (Qase ID: 7) + +JSONPlaceholder Error Handling + ✓ GET non-existent user - verify 404 response (Qase ID: 8) + ✓ GET non-existent post - attach error response (Qase ID: 9) + ✓ Invalid endpoint - verify graceful 404 handling (Qase ID: 10) + +JSONPlaceholder Advanced Qase Features + ✓ Complex nested steps - multi-resource retrieval (Qase ID: 11) + ✓ Suite hierarchy demonstration (Qase ID: 12) + ✓ Parameterized test pattern - multiple user IDs (Qase ID: 13) + - Future feature - API authentication (Qase ID: 14) + +13 passing +1 pending + +[INFO] qase: Test run link: https://app.qase.io/run/YOUR_PROJECT/dashboard/RUN_ID +``` ## Additional Resources -For more details on how to use this integration with Qase Test Management, visit -the [Qase Mocha documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-mocha). +- [Qase Mocha Reporter Documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-mocha) +- [Mocha Documentation](https://mochajs.org/) +- [JSONPlaceholder Guide](https://jsonplaceholder.typicode.com/guide/) diff --git a/examples/single/mocha/package.json b/examples/single/mocha/package.json index 1b9bc510..c660fb7c 100644 --- a/examples/single/mocha/package.json +++ b/examples/single/mocha/package.json @@ -2,10 +2,10 @@ "name": "examples-mocha", "private": true, "scripts": { - "test": "QASE_MODE=testops mocha", - "test:parallel": "QASE_MODE=testops mocha --parallel", - "test:extra": "QASE_MODE=testops mocha --reporter mocha-qase-reporter --reporter-options extraReporters=spec", - "test:extra-parallel": "QASE_MODE=testops mocha --reporter mocha-qase-reporter --reporter-options extraReporters=spec --parallel" + "test": "QASE_MODE=${QASE_MODE:-off} mocha", + "test:parallel": "QASE_MODE=${QASE_MODE:-off} mocha --parallel", + "test:extra": "QASE_MODE=${QASE_MODE:-off} mocha --reporter mocha-qase-reporter --reporter-options extraReporters=spec", + "test:extra-parallel": "QASE_MODE=${QASE_MODE:-off} mocha --reporter mocha-qase-reporter --reporter-options extraReporters=spec --parallel" }, "devDependencies": { "mocha": "^10.8.2", diff --git a/examples/single/mocha/test/api-advanced.spec.js b/examples/single/mocha/test/api-advanced.spec.js new file mode 100644 index 00000000..c95b4b74 --- /dev/null +++ b/examples/single/mocha/test/api-advanced.spec.js @@ -0,0 +1,138 @@ +const { qase } = require('mocha-qase-reporter/mocha'); +const assert = require('assert'); + +describe('JSONPlaceholder Advanced Qase Features', function() { + const BASE_URL = 'https://jsonplaceholder.typicode.com'; + + it(qase(11, 'Complex nested steps - multi-resource retrieval'), async function() { + qase.fields({ layer: 'api', severity: 'normal', priority: 'medium' }); + + let user; + let userPosts; + let firstPostComments; + + await qase.step('Retrieve user and their content', async () => { + await qase.step('Fetch user data', async () => { + const response = await fetch(`${BASE_URL}/users/1`); + assert.strictEqual(response.status, 200); + user = await response.json(); + assert.strictEqual(user.name, 'Leanne Graham'); + }); + + await qase.step('Fetch user posts', async () => { + const response = await fetch(`${BASE_URL}/posts?userId=${user.id}`); + assert.strictEqual(response.status, 200); + userPosts = await response.json(); + assert.ok(userPosts.length > 0, 'User should have posts'); + }); + + await qase.step('Fetch comments for first post', async () => { + const firstPost = userPosts[0]; + const response = await fetch(`${BASE_URL}/posts/${firstPost.id}/comments`); + assert.strictEqual(response.status, 200); + firstPostComments = await response.json(); + assert.ok(firstPostComments.length > 0, 'Post should have comments'); + }); + }); + + await qase.step('Verify data relationships', async () => { + await qase.step('Verify all posts belong to user', async () => { + userPosts.forEach(post => { + assert.strictEqual(post.userId, user.id, 'All posts should belong to the user'); + }); + }); + + await qase.step('Verify all comments belong to the post', async () => { + const firstPostId = userPosts[0].id; + firstPostComments.forEach(comment => { + assert.strictEqual(comment.postId, firstPostId, 'All comments should belong to the post'); + }); + }); + }); + + qase.comment(`Retrieved data for user "${user.name}" with ${userPosts.length} posts and ${firstPostComments.length} comments on first post`); + }); + + it(qase(12, 'Suite hierarchy demonstration'), async function() { + qase.suite('API Tests\tAdvanced\tRelationships'); + qase.fields({ layer: 'api', severity: 'low' }); + + let albums; + let photos; + + await qase.step('Test album and photo relationships', async () => { + const albumResponse = await fetch(`${BASE_URL}/albums/1`); + const album = await albumResponse.json(); + assert.strictEqual(album.id, 1); + + const photosResponse = await fetch(`${BASE_URL}/albums/1/photos`); + photos = await photosResponse.json(); + assert.ok(photos.length > 0, 'Album should have photos'); + + albums = [album]; + }); + + await qase.step('Verify photo structure', async () => { + const firstPhoto = photos[0]; + assert.ok(firstPhoto.id, 'Photo should have id'); + assert.ok(firstPhoto.albumId, 'Photo should have albumId'); + assert.ok(firstPhoto.title, 'Photo should have title'); + assert.ok(firstPhoto.url, 'Photo should have url'); + assert.ok(firstPhoto.thumbnailUrl, 'Photo should have thumbnailUrl'); + }); + + qase.attach({ + name: 'album-photos-sample.json', + content: JSON.stringify({ + album: albums[0], + photoCount: photos.length, + samplePhotos: photos.slice(0, 3) + }, null, 2), + contentType: 'application/json' + }); + }); + + it(qase(13, 'Parameterized test pattern - multiple user IDs'), async function() { + const userIds = [1, 2, 3]; + qase.parameters({ userIds: userIds.join(', ') }); + + let allUsers = []; + + await qase.step('Fetch multiple users by ID', async () => { + for (const userId of userIds) { + await qase.step(`Fetch user ${userId}`, async () => { + const response = await fetch(`${BASE_URL}/users/${userId}`); + assert.strictEqual(response.status, 200); + const user = await response.json(); + assert.strictEqual(user.id, userId); + allUsers.push(user); + }); + } + }); + + await qase.step('Verify all users were retrieved', async () => { + assert.strictEqual(allUsers.length, userIds.length, 'Should have retrieved all requested users'); + allUsers.forEach((user, index) => { + assert.strictEqual(user.id, userIds[index], `User ${index} should have correct ID`); + }); + }); + + qase.attach({ + name: 'multiple-users.json', + content: JSON.stringify(allUsers, null, 2), + contentType: 'application/json' + }); + }); + + it.skip(qase(14, 'Future feature - API authentication'), function() { + qase.ignore(); + qase.comment('This test is ignored as JSONPlaceholder does not support authentication. Future implementation for authenticated APIs.'); + + // Placeholder for future authentication testing + // When testing real APIs with authentication: + // - Test token/API key validation + // - Test expired token handling + // - Test unauthorized access attempts + // - Test role-based access control + }); +}); diff --git a/examples/single/mocha/test/api-crud.spec.js b/examples/single/mocha/test/api-crud.spec.js new file mode 100644 index 00000000..dcf708ad --- /dev/null +++ b/examples/single/mocha/test/api-crud.spec.js @@ -0,0 +1,141 @@ +const { qase } = require('mocha-qase-reporter/mocha'); +const assert = require('assert'); + +describe('JSONPlaceholder User CRUD Operations', function() { + const BASE_URL = 'https://jsonplaceholder.typicode.com'; + + it(qase(1, 'GET all users - verify 10 users returned'), async function() { + qase.fields({ layer: 'api', severity: 'normal' }); + + let response; + let users; + + await qase.step('Send GET request to /users endpoint', async () => { + response = await fetch(`${BASE_URL}/users`); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200, 'Expected status code 200'); + }); + + await qase.step('Parse JSON response', async () => { + users = await response.json(); + }); + + await qase.step('Verify 10 users are returned', async () => { + assert.strictEqual(Array.isArray(users), true, 'Response should be an array'); + assert.strictEqual(users.length, 10, 'Should have exactly 10 users'); + }); + + await qase.step('Verify user structure has required fields', async () => { + const firstUser = users[0]; + assert.ok(firstUser.id, 'User should have id'); + assert.ok(firstUser.name, 'User should have name'); + assert.ok(firstUser.email, 'User should have email'); + assert.ok(firstUser.username, 'User should have username'); + }); + }); + + it(qase(2, 'GET single user by ID - verify user details'), async function() { + qase.parameters({ userId: 1 }); + + let response; + let user; + + await qase.step('Send GET request to /users/1', async () => { + response = await fetch(`${BASE_URL}/users/1`); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200); + }); + + await qase.step('Parse user data', async () => { + user = await response.json(); + }); + + await qase.step('Verify user is Leanne Graham', async () => { + assert.strictEqual(user.id, 1, 'User ID should be 1'); + assert.strictEqual(user.name, 'Leanne Graham', 'User name should be Leanne Graham'); + assert.strictEqual(user.email, 'Sincere@april.biz', 'Email should match expected value'); + assert.strictEqual(user.username, 'Bret', 'Username should be Bret'); + }); + + await qase.step('Verify user has address and company details', async () => { + assert.ok(user.address, 'User should have address'); + assert.ok(user.company, 'User should have company'); + assert.ok(user.address.city, 'Address should have city'); + assert.ok(user.company.name, 'Company should have name'); + }); + }); + + it(qase(3, 'POST create user - verify 201 response and returned ID'), async function() { + const newUser = { + name: 'John Doe', + username: 'johndoe', + email: 'john.doe@example.com', + phone: '123-456-7890', + website: 'johndoe.com' + }; + + qase.attach({ + name: 'new-user-request.json', + content: JSON.stringify(newUser, null, 2), + contentType: 'application/json' + }); + + let response; + let createdUser; + + await qase.step('Send POST request to create user', async () => { + response = await fetch(`${BASE_URL}/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newUser) + }); + }); + + await qase.step('Verify response status is 201 (Created)', async () => { + assert.strictEqual(response.status, 201, 'Expected status code 201 for created resource'); + }); + + await qase.step('Parse created user data', async () => { + createdUser = await response.json(); + }); + + await qase.step('Verify returned user has ID and matches request data', async () => { + assert.ok(createdUser.id, 'Created user should have an ID'); + assert.strictEqual(createdUser.name, newUser.name, 'Name should match'); + assert.strictEqual(createdUser.email, newUser.email, 'Email should match'); + assert.strictEqual(createdUser.username, newUser.username, 'Username should match'); + }); + + qase.attach({ + name: 'created-user-response.json', + content: JSON.stringify(createdUser, null, 2), + contentType: 'application/json' + }); + }); + + it(qase(4, 'DELETE user - verify 200 response'), async function() { + qase.comment('Note: JSONPlaceholder fakes DELETE operations - data is not actually deleted'); + + let response; + + await qase.step('Send DELETE request for user ID 1', async () => { + response = await fetch(`${BASE_URL}/users/1`, { + method: 'DELETE' + }); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200, 'DELETE should return 200 status'); + }); + + await qase.step('Verify empty response body', async () => { + const body = await response.json(); + // JSONPlaceholder returns empty object {} for successful DELETE + assert.strictEqual(typeof body, 'object', 'Response should be an object'); + }); + }); +}); diff --git a/examples/single/mocha/test/api-errors.spec.js b/examples/single/mocha/test/api-errors.spec.js new file mode 100644 index 00000000..b1a4f6f2 --- /dev/null +++ b/examples/single/mocha/test/api-errors.spec.js @@ -0,0 +1,94 @@ +const { qase } = require('mocha-qase-reporter/mocha'); +const assert = require('assert'); + +describe('JSONPlaceholder Error Handling', function() { + const BASE_URL = 'https://jsonplaceholder.typicode.com'; + + it(qase(8, 'GET non-existent user - verify 404 response'), async function() { + qase.comment('Testing error handling for non-existent resource - this is an expected failure scenario'); + + const nonExistentUserId = 9999; + let response; + + await qase.step(`Send GET request to /users/${nonExistentUserId}`, async () => { + response = await fetch(`${BASE_URL}/users/${nonExistentUserId}`); + }); + + await qase.step('Verify response status is 404 (Not Found)', async () => { + assert.strictEqual( + response.status, + 404, + 'Should return 404 for non-existent user' + ); + }); + + await qase.step('Verify response body is empty object', async () => { + const body = await response.json(); + assert.strictEqual(typeof body, 'object', 'Response should be an object'); + assert.strictEqual(Object.keys(body).length, 0, 'Response should be empty for 404'); + }); + }); + + it(qase(9, 'GET non-existent post - attach error response'), async function() { + const nonExistentPostId = 99999; + let response; + let errorBody; + + await qase.step(`Send GET request to /posts/${nonExistentPostId}`, async () => { + response = await fetch(`${BASE_URL}/posts/${nonExistentPostId}`); + }); + + await qase.step('Verify response status is 404', async () => { + assert.strictEqual(response.status, 404); + }); + + await qase.step('Parse error response', async () => { + errorBody = await response.json(); + }); + + await qase.step('Attach error response for documentation', async () => { + qase.attach({ + name: '404-error-response.json', + content: JSON.stringify({ + status: response.status, + statusText: response.statusText, + body: errorBody, + url: response.url + }, null, 2), + contentType: 'application/json' + }); + }); + + await qase.step('Verify error response structure', async () => { + assert.strictEqual(typeof errorBody, 'object', 'Error body should be an object'); + }); + }); + + it(qase(10, 'Invalid endpoint - verify graceful 404 handling'), async function() { + const invalidEndpoint = '/invalid-endpoint-12345'; + let response; + + await qase.step(`Send GET request to invalid endpoint: ${invalidEndpoint}`, async () => { + response = await fetch(`${BASE_URL}${invalidEndpoint}`); + }); + + await qase.step('Verify response status is 404', async () => { + assert.strictEqual( + response.status, + 404, + 'Invalid endpoint should return 404' + ); + }); + + await qase.step('Verify API handles invalid endpoint gracefully', async () => { + // Should not throw error, just return 404 + assert.ok(response, 'Response should exist even for invalid endpoint'); + assert.strictEqual(response.ok, false, 'Response should not be ok for 404'); + }); + + await qase.step('Verify can still parse response body', async () => { + const body = await response.json(); + assert.strictEqual(typeof body, 'object', 'Should return object even for error'); + }); + }); +}); diff --git a/examples/single/mocha/test/api-posts.spec.js b/examples/single/mocha/test/api-posts.spec.js new file mode 100644 index 00000000..cd5188ca --- /dev/null +++ b/examples/single/mocha/test/api-posts.spec.js @@ -0,0 +1,125 @@ +const { qase } = require('mocha-qase-reporter/mocha'); +const assert = require('assert'); + +describe('JSONPlaceholder Post Validation', function() { + const BASE_URL = 'https://jsonplaceholder.typicode.com'; + + it(qase(5, 'GET all posts - verify 100 posts returned'), async function() { + qase.fields({ priority: 'high' }); + + let response; + let posts; + + await qase.step('Send GET request to /posts endpoint', async () => { + response = await fetch(`${BASE_URL}/posts`); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200); + }); + + await qase.step('Parse posts data', async () => { + posts = await response.json(); + }); + + await qase.step('Verify 100 posts are returned', async () => { + assert.strictEqual(Array.isArray(posts), true, 'Response should be an array'); + assert.strictEqual(posts.length, 100, 'Should have exactly 100 posts'); + }); + + await qase.step('Verify post structure', async () => { + const firstPost = posts[0]; + assert.ok(firstPost.id, 'Post should have id'); + assert.ok(firstPost.userId, 'Post should have userId'); + assert.ok(firstPost.title, 'Post should have title'); + assert.ok(firstPost.body, 'Post should have body'); + }); + }); + + it(qase(6, 'GET posts by user ID - verify filtered results'), async function() { + const testUserId = 1; + qase.parameters({ userId: testUserId }); + + let response; + let posts; + + await qase.step(`Send GET request to /posts?userId=${testUserId}`, async () => { + response = await fetch(`${BASE_URL}/posts?userId=${testUserId}`); + }); + + await qase.step('Verify response status is 200', async () => { + assert.strictEqual(response.status, 200); + }); + + await qase.step('Parse filtered posts', async () => { + posts = await response.json(); + }); + + await qase.step('Verify all posts belong to the specified user', async () => { + assert.strictEqual(Array.isArray(posts), true, 'Response should be an array'); + assert.ok(posts.length > 0, 'Should have at least one post'); + + posts.forEach((post, index) => { + assert.strictEqual( + post.userId, + testUserId, + `Post ${index} should belong to user ${testUserId}` + ); + }); + }); + + await qase.step('Verify post count for user 1 is 10', async () => { + assert.strictEqual(posts.length, 10, 'User 1 should have exactly 10 posts'); + }); + }); + + it(qase(7, 'GET post with comments - verify comment structure'), async function() { + const testPostId = 1; + + let postResponse; + let post; + let commentsResponse; + let comments; + + await qase.step(`Send GET request to /posts/${testPostId}`, async () => { + postResponse = await fetch(`${BASE_URL}/posts/${testPostId}`); + post = await postResponse.json(); + }); + + await qase.step('Verify post was retrieved successfully', async () => { + assert.strictEqual(postResponse.status, 200); + assert.strictEqual(post.id, testPostId); + assert.ok(post.title, 'Post should have a title'); + }); + + await qase.step(`Send GET request to /posts/${testPostId}/comments`, async () => { + commentsResponse = await fetch(`${BASE_URL}/posts/${testPostId}/comments`); + }); + + await qase.step('Verify comments response status is 200', async () => { + assert.strictEqual(commentsResponse.status, 200); + }); + + await qase.step('Parse comments data', async () => { + comments = await commentsResponse.json(); + }); + + await qase.step('Verify comment count and structure', async () => { + assert.strictEqual(Array.isArray(comments), true, 'Comments should be an array'); + assert.ok(comments.length > 0, 'Post should have comments'); + + const firstComment = comments[0]; + assert.strictEqual(firstComment.postId, testPostId, 'Comment should belong to the post'); + assert.ok(firstComment.id, 'Comment should have id'); + assert.ok(firstComment.name, 'Comment should have name'); + assert.ok(firstComment.email, 'Comment should have email'); + assert.ok(firstComment.body, 'Comment should have body'); + }); + + qase.attach({ + name: 'post-with-comments.json', + content: JSON.stringify({ post, comments }, null, 2), + contentType: 'application/json' + }); + }); +}); diff --git a/examples/single/mocha/test/async.spec.js b/examples/single/mocha/test/async.spec.js deleted file mode 100644 index ce010d90..00000000 --- a/examples/single/mocha/test/async.spec.js +++ /dev/null @@ -1,74 +0,0 @@ -const assert = require('assert'); -const { qase } = require('mocha-qase-reporter/mocha'); - - -describe('Simple tests async', function() { - // this.timeout(60000); - - it('test without qase metadata success', async function() { - assert.strictEqual(1, 1); - }); - - it('test without qase metadata failed', async function() { - assert.strictEqual(1, 2); - }); - - it.skip(qase(10, 'test with qase id success'), async function() { - assert.strictEqual(1, 1); - }); - - it(qase(20, 'test with qase id failed'), async function() { - assert.strictEqual(1, 2); - }); - - it('test with title success', async function() { - this.title('Successful test with title'); - assert.strictEqual(1, 1); - }); - - it('test with title failed', async function() { - this.title('Failing test with title'); - assert.strictEqual(1, 2); - }); - - it('test with suite success', async function() { - this.suite('Suite 1 async'); - assert.strictEqual(1, 1); - }); - - it('test with suite failed', async function() { - this.suite('Suite 1 async'); - assert.strictEqual(1, 2); - }); - - it('test with comment success', async function() { - this.comment('comment'); - assert.strictEqual(1, 1); - }); - - it('test with comment failed', async function() { - this.comment('comment'); - assert.strictEqual(1, 2); - }); - - it('test with fields success', async function() { - this.fields({ custom_field: 'value' }); - assert.strictEqual(1, 1); - }); - - it('test with fields failed', async function() { - this.fields({ custom_field: 'value' }); - assert.strictEqual(1, 2); - }); - - it('ignored test success', async function() { - this.ignore(); - assert.strictEqual(1, 1); - }); - - it('ignored test failed', async function() { - this.ignore(); - assert.strictEqual(1, 2); - }); -}); - diff --git a/examples/single/mocha/test/attachTests.spec.js b/examples/single/mocha/test/attachTests.spec.js deleted file mode 100644 index ea572647..00000000 --- a/examples/single/mocha/test/attachTests.spec.js +++ /dev/null @@ -1,14 +0,0 @@ -const assert = require('assert'); - - -describe('Attachment tests', function() { - it('successful test with string attachment', function() { - this.attach({name:"attachment.log", content:"data", contentType:"text/plain"}); - assert.strictEqual(1, 1); - }); - - it('failing test with string attachment', function() { - this.attach({name:"attachment.log", content:"data", contentType:"text/plain"}); - assert.strictEqual(1, 2); - }); -}); diff --git a/examples/single/mocha/test/parametrizedTests.spec.js b/examples/single/mocha/test/parametrizedTests.spec.js deleted file mode 100644 index 8537a49d..00000000 --- a/examples/single/mocha/test/parametrizedTests.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -const assert = require('assert'); - -describe('Parametrized test', function() { - const params = [1, 2, 3, 4, 5]; - params.forEach((param) => { - it(`test with parameters success ${param}`, function() { - this.parameters({ number: param }); - assert.strictEqual(param, param); - }); - it(`test with parameters failed ${param}`, function() { - this.parameters({ number: param }); - assert.strictEqual(param, param + 1); - }); - }); -}); diff --git a/examples/single/mocha/test/simpleTests.spec.js b/examples/single/mocha/test/simpleTests.spec.js deleted file mode 100644 index 388f8fe5..00000000 --- a/examples/single/mocha/test/simpleTests.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -const assert = require('assert'); -const { qase } = require('mocha-qase-reporter/mocha'); - - -describe('Simple tests', function() { - it('test without qase metadata success', function() { - assert.strictEqual(1, 1); - }); - - it('test without qase metadata failed', function() { - assert.strictEqual(1, 2); - }); - - it.skip(qase(1, 'test with qase id success'), function() { - assert.strictEqual(1, 1); - }); - - it(qase(2, 'test with qase id failed'), function() { - assert.strictEqual(1, 2); - }); - - it('test with title success', function() { - this.title('Successful test with title'); - assert.strictEqual(1, 1); - }); - - it('test with title failed', function() { - this.title('Failing test with title'); - assert.strictEqual(1, 2); - }); - - it('test with suite success', function() { - this.suite('Suite 1'); - assert.strictEqual(1, 1); - }); - - it('test with suite failed', function() { - this.suite('Suite 1'); - assert.strictEqual(1, 2); - }); - - it('test with comment success', function() { - this.comment('comment'); - assert.strictEqual(1, 1); - }); - - it('test with comment failed', function() { - this.comment('comment'); - assert.strictEqual(1, 2); - }); - - it('test with fields success', function() { - this.fields({ custom_field: 'value' }); - assert.strictEqual(1, 1); - }); - - it('test with fields failed', function() { - this.fields({ custom_field: 'value' }); - assert.strictEqual(1, 2); - }); - - it('ignored test success', function() { - this.ignore(); - assert.strictEqual(1, 1); - }); - - it('ignored test failed', function() { - this.ignore(); - assert.strictEqual(1, 2); - }); -}); - diff --git a/examples/single/mocha/test/stepTests.spec.js b/examples/single/mocha/test/stepTests.spec.js deleted file mode 100644 index 319613a8..00000000 --- a/examples/single/mocha/test/stepTests.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -const assert = require('assert'); - - -describe('Step tests', function() { - it('successful test with steps', function() { - this.step('step 1', function() {}); - this.step('step 2', function() {}); - assert.strictEqual(1, 1); - }); - - it('failing test with steps', function() { - this.step('step 1', function() {}); - this.step('step 2', function() {}); - assert.strictEqual(1, 2); - }); - - it('test with steps including expected results and data', function() { - this.step('Click button', function() { - // Click action - }, 'Button should be clicked', 'Button data'); - - this.step('Fill form', function() { - // Form filling action - }, 'Form should be filled', 'Form input data'); - - assert.strictEqual(1, 1); - }); -}); diff --git a/examples/single/newman/README.md b/examples/single/newman/README.md index 50c4b3ec..30420ac6 100644 --- a/examples/single/newman/README.md +++ b/examples/single/newman/README.md @@ -1,92 +1,161 @@ -# Newman Example +# Newman Collection Example -This is a sample project demonstrating how to run Postman collections using the Newman CLI runner with integration to Qase Test Management. +## Overview -## Prerequisites +This example demonstrates realistic API testing using a Postman collection with the Newman CLI runner and Qase Test Management integration. Tests exercise the JSONPlaceholder REST API with organized collection folders providing suite hierarchy. All test cases are annotated using comment-based syntax, and the collection structure automatically provides suite organization. -Ensure that the following tools are installed on your machine: +## Prerequisites -1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) -2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) +1. [Node.js](https://nodejs.org/) (version 18 or higher recommended) +2. [npm](https://www.npmjs.com/) -## Setup Instructions +## Installation -1. Clone this repository by running the following commands: +1. Clone the repository: ```bash git clone https://github.com/qase-tms/qase-javascript.git cd qase-javascript/examples/single/newman ``` -2. Install the project dependencies: +2. Install dependencies: ```bash npm install ``` -3. Create a `qase.config.json` file in the root of the project. Follow the instructions on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). - -## Example Files - -This example includes: - -* **sample-collection.json** — Postman collection with test assertions - * Contains examples with and without Qase ID comments - * Demonstrates comment-based test case linking -* **qase.config.json** — Qase reporter configuration +3. Configure Qase credentials in `qase.config.json`: + - Set your API token in `testops.api.token` + - Set your project code in `testops.project` + +## Configuration + +The Qase reporter can be configured using environment variables or configuration files. + +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) + +Example `qase.config.json`: + +```json +{ + "debug": true, + "testops": { + "api": { + "token": "your_api_token_here" + }, + "project": "YOUR_PROJECT_CODE", + "uploadAttachments": false, + "run": { + "complete": true, + "title": "Newman API Test Run" + } + } +} +``` ## Running Tests -To run tests locally without reporting to Qase: - ```bash -QASE_MODE=off npm test +# Run tests without Qase reporting (default) +npm test + +# Run tests with Qase reporting +QASE_MODE=testops npm test ``` -To run tests and upload the results to Qase Test Management: +### Run with parameterized data file ```bash -npm test +npm run test:data ``` -Or with explicit mode: +This runs the collection 3 times (one per data row), with each iteration using different `userId` and `expectedName` values. -```bash -QASE_MODE=testops npx newman run sample-collection.json -r qase -``` +## Collection Structure -## Expected Behavior +### Users (4 requests) +CRUD operations on JSONPlaceholder users: +- **Get all users** -- Verify 10 users with required fields (id, name, email, address) +- **Get single user** -- Verify user 1 data and nested address structure +- **Create user** -- POST with JSON body, verify 201 response and returned user +- **Delete user** -- DELETE request, verify 200 and empty response body -When tests execute with Qase reporting enabled: +### Posts (3 requests) +Post validation and filtering: +- **Get all posts** -- Verify 100 posts with required fields (userId, id, title, body) +- **Filter posts by user** -- Query string filtering with parameterized userId +- **Get post comments** -- Nested resource, verify 5 comments with valid email -* **Each pm.test()** in the collection is reported as a separate test result -* **Comment-based IDs** (`// qase: 123`) link tests to existing Qase test cases -* **Postman assertions** determine pass/fail status -* **Collection structure** (folders) organizes tests in Qase -* **Request/response data** is captured in test result details +### Error Handling (3 requests) +Error and edge case scenarios: +- **Non-existent user** -- Verify 200 with empty object for /users/999 +- **Invalid endpoint** -- Verify 404 for unknown routes +- **POST with empty body** -- Verify graceful handling (201 with generated ID) -## Limitations +### Advanced (3 requests) +Advanced testing patterns: +- **Chained request** -- Pre-request script fetches user, stores in collection variable, test validates +- **Parameterized user lookup** -- Data-driven testing with `// qase.parameters:` annotation +- **Response time validation** -- Performance assertion (response under 2000ms) + +## Qase Features Demonstrated + +| Feature | How It's Used | Example | +|---------|---------------|---------| +| Test Case ID | `// qase: N` comment before pm.test() | `// qase: 1` | +| Parameters | `// qase.parameters: key1, key2` comment | `// qase.parameters: userId, expectedName` | +| Auto-collect Params | `autoCollectParams: true` in qase.config.json | Reports all data file fields automatically | +| Suite Hierarchy | Collection folder structure | `JSONPlaceholder API Tests > Users > Get all users` | +| Data-driven Testing | `-d data.json` flag with iteration data | 3 iterations with different userId/expectedName | + +## Newman-Specific Patterns -Newman reporter has the following limitations compared to other frameworks: +- **Comment-based annotations** -- Use `// qase: N` in exec array before pm.test() calls +- **Each pm.test() is a separate result** -- No nesting or step hierarchy +- **Folder = Suite** -- Collection folders automatically create suite hierarchy in Qase +- **Pre-request scripts** -- Run before the main request; useful for chaining and setup +- **Data-driven iterations** -- `-d data.json` runs collection once per data row +- **Collection variables** -- Share data between pre-request and test scripts -* **No programmatic steps API** — All assertions are reported at the test level (no qase.step()) -* **No programmatic attachments API** — No qase.attach() support (Postman security/portability constraint) -* **No custom fields support** — Severity, priority, and other fields cannot be set via comments -* **Comment-based only** — Test configuration uses special comments, not programmatic imports +## Project Structure + +``` +newman/ +├── api-collection.json # Postman collection with tests +├── data.json # Data file for parameterized tests +├── qase.config.json # Qase reporter configuration +└── package.json +``` + +## Limitations -### Workarounds +Newman reporter has limited Qase feature support compared to other frameworks: -* **For step-like organization:** Use multiple `pm.test()` calls with descriptive names -* **For attachments:** Use Postman console logging or store data in collection variables -* **For test organization:** Use Postman collection folders to group related tests +| Feature | Supported | Notes | +|---------|-----------|-------| +| Test Case ID | Yes | Via `// qase: N` comments | +| Parameters | Yes | Via `// qase.parameters:` + data file | +| Suite Hierarchy | Yes | Via collection folder structure | +| Title Override | No | Test name comes from request name | +| Custom Fields | No | No severity, priority, etc. | +| Steps | No | Each pm.test() is a separate result | +| Attachments | No | No file attachment support | +| Ignore | No | Cannot exclude specific tests | +| Comments | No | No comment annotation support | -## Framework-Specific Features +This is the most limited Qase reporter in the JavaScript ecosystem, but still provides essential test case linking and parameterization for Newman/Postman workflows. -Newman with Qase has unique patterns: +## API Notes -* **Comment-based annotations** — Use `// qase: 123` comments before `pm.test()` calls -* **Data-driven testing** — Run with `-d data.json` for parameterized tests -* **Multiple reporters** — Combine with other Newman reporters (`-r cli,qase`) -* **Collection-level config** — Parameters can be specified at folder/collection level +Tests use [JSONPlaceholder](https://jsonplaceholder.typicode.com/) as the test API: +- Free, public REST API -- no authentication required +- Returns realistic data (users, posts, comments, todos) +- Write operations (POST, PUT, DELETE) are faked -- they return success responses but don't persist data +- Stable and widely used for testing and prototyping ## Additional Resources -For more details on how to use this integration with Qase Test Management, visit the [Qase Newman documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-newman). +- [Qase Newman Reporter](https://github.com/qase-tms/qase-javascript/tree/main/qase-newman) +- [Newman Documentation](https://learning.postman.com/docs/collections/using-newman-cli/command-line-integration-with-newman/) +- [JSONPlaceholder API Guide](https://jsonplaceholder.typicode.com/guide/) diff --git a/examples/single/newman/api-collection.json b/examples/single/newman/api-collection.json new file mode 100644 index 00000000..edc32f87 --- /dev/null +++ b/examples/single/newman/api-collection.json @@ -0,0 +1,551 @@ +{ + "info": { + "name": "JSONPlaceholder API Tests", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "description": "Comprehensive API test collection demonstrating Qase Newman integration with JSONPlaceholder REST API" + }, + "item": [ + { + "name": "Users", + "description": "User CRUD operations", + "item": [ + { + "name": "Get all users", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 1", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Response contains 10 users', function () {", + " var users = pm.response.json();", + " pm.expect(users).to.be.an('array');", + " pm.expect(users.length).to.eql(10);", + "});", + "", + "pm.test('Users have required fields', function () {", + " var user = pm.response.json()[0];", + " pm.expect(user).to.have.property('id');", + " pm.expect(user).to.have.property('name');", + " pm.expect(user).to.have.property('email');", + " pm.expect(user).to.have.property('address');", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/users", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users"] + } + } + }, + { + "name": "Get single user", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 2", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('User is Leanne Graham', function () {", + " var user = pm.response.json();", + " pm.expect(user.name).to.eql('Leanne Graham');", + " pm.expect(user.email).to.eql('Sincere@april.biz');", + "});", + "", + "pm.test('User has nested address', function () {", + " var user = pm.response.json();", + " pm.expect(user.address).to.have.property('street');", + " pm.expect(user.address).to.have.property('city');", + " pm.expect(user.address.geo).to.have.property('lat');", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/users/1", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users", "1"] + } + } + }, + { + "name": "Create user", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 3", + "pm.test('Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test('Response contains new user ID', function () {", + " var user = pm.response.json();", + " pm.expect(user).to.have.property('id');", + " pm.expect(user.name).to.eql('Test User');", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test User\",\n \"username\": \"testuser\",\n \"email\": \"test@example.com\"\n}" + }, + "url": { + "raw": "https://jsonplaceholder.typicode.com/users", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users"] + } + } + }, + { + "name": "Delete user", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 4", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Response is empty object', function () {", + " var body = pm.response.json();", + " pm.expect(Object.keys(body).length).to.eql(0);", + "});" + ] + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/users/1", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users", "1"] + } + } + } + ] + }, + { + "name": "Posts", + "description": "Post validation and filtering", + "item": [ + { + "name": "Get all posts", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 5", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Response contains 100 posts', function () {", + " var posts = pm.response.json();", + " pm.expect(posts).to.be.an('array');", + " pm.expect(posts.length).to.eql(100);", + "});", + "", + "pm.test('Posts have required fields', function () {", + " var post = pm.response.json()[0];", + " pm.expect(post).to.have.property('userId');", + " pm.expect(post).to.have.property('id');", + " pm.expect(post).to.have.property('title');", + " pm.expect(post).to.have.property('body');", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/posts", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["posts"] + } + } + }, + { + "name": "Filter posts by user", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 6", + "// qase.parameters: userId", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('All posts belong to requested user', function () {", + " var posts = pm.response.json();", + " pm.expect(posts).to.be.an('array');", + " pm.expect(posts.length).to.be.above(0);", + " posts.forEach(function (post) {", + " pm.expect(post.userId).to.eql(pm.iterationData.get('userId') || 1);", + " });", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/posts?userId=1", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["posts"], + "query": [ + { + "key": "userId", + "value": "1" + } + ] + } + } + }, + { + "name": "Get post comments", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 7", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Post has 5 comments', function () {", + " var comments = pm.response.json();", + " pm.expect(comments).to.be.an('array');", + " pm.expect(comments.length).to.eql(5);", + "});", + "", + "pm.test('Comments have valid email', function () {", + " var comments = pm.response.json();", + " comments.forEach(function (comment) {", + " pm.expect(comment).to.have.property('email');", + " pm.expect(comment.email).to.include('@');", + " });", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/posts/1/comments", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["posts", "1", "comments"] + } + } + } + ] + }, + { + "name": "Error Handling", + "description": "Error and edge case scenarios", + "item": [ + { + "name": "Non-existent user", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 8", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Response is empty object', function () {", + " var body = pm.response.json();", + " pm.expect(Object.keys(body).length).to.eql(0);", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/users/999", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users", "999"] + } + } + }, + { + "name": "Invalid endpoint", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 9", + "pm.test('Status code is 404', function () {", + " pm.response.to.have.status(404);", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/invalid-endpoint", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["invalid-endpoint"] + } + } + }, + { + "name": "POST with empty body", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 10", + "pm.test('Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test('Response contains generated ID', function () {", + " var body = pm.response.json();", + " pm.expect(body).to.have.property('id');", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "https://jsonplaceholder.typicode.com/posts", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["posts"] + } + } + } + ] + }, + { + "name": "Advanced", + "description": "Advanced testing patterns", + "item": [ + { + "name": "Chained request - get user then posts", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Pre-request script: fetch user first, store name", + "pm.sendRequest('https://jsonplaceholder.typicode.com/users/1', function (err, res) {", + " if (!err) {", + " pm.collectionVariables.set('userName', res.json().name);", + " }", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 11", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Posts belong to expected user', function () {", + " var posts = pm.response.json();", + " pm.expect(posts).to.be.an('array');", + " pm.expect(posts.length).to.be.above(0);", + " posts.forEach(function (post) {", + " pm.expect(post.userId).to.eql(1);", + " });", + "});", + "", + "pm.test('Pre-request captured user name', function () {", + " var userName = pm.collectionVariables.get('userName');", + " pm.expect(userName).to.eql('Leanne Graham');", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/posts?userId=1", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["posts"], + "query": [ + { + "key": "userId", + "value": "1" + } + ] + } + } + }, + { + "name": "Parameterized user lookup", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 12", + "// qase.parameters: userId, expectedName", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('User matches expected name', function () {", + " var user = pm.response.json();", + " var expectedName = pm.iterationData.get('expectedName') || 'Leanne Graham';", + " pm.expect(user.name).to.eql(expectedName);", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/users/1", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users", "1"] + } + } + }, + { + "name": "Response time validation", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// qase: 13", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Response time is under 2000ms', function () {", + " pm.expect(pm.response.responseTime).to.be.below(2000);", + "});", + "", + "pm.test('Response has valid structure', function () {", + " var users = pm.response.json();", + " pm.expect(users).to.be.an('array');", + " pm.expect(users.length).to.be.above(0);", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://jsonplaceholder.typicode.com/users", + "protocol": "https", + "host": ["jsonplaceholder", "typicode", "com"], + "path": ["users"] + } + } + } + ] + } + ], + "variable": [ + { + "key": "userName", + "value": "" + } + ] +} diff --git a/examples/single/newman/data.json b/examples/single/newman/data.json new file mode 100644 index 00000000..acbbc9df --- /dev/null +++ b/examples/single/newman/data.json @@ -0,0 +1,5 @@ +[ + { "userId": 1, "expectedName": "Leanne Graham" }, + { "userId": 2, "expectedName": "Ervin Howell" }, + { "userId": 3, "expectedName": "Clementine Bauch" } +] diff --git a/examples/single/newman/package.json b/examples/single/newman/package.json index 08689f2d..c823850b 100644 --- a/examples/single/newman/package.json +++ b/examples/single/newman/package.json @@ -2,10 +2,11 @@ "name": "examples-newman", "private": true, "scripts": { - "test": "QASE_MODE=testops newman run ./sample-collection.json -r qase" + "test": "QASE_MODE=${QASE_MODE:-off} newman run ./api-collection.json -r qase", + "test:data": "QASE_MODE=${QASE_MODE:-off} newman run ./api-collection.json -r qase -d data.json" }, "devDependencies": { "newman": "^6.2.1", - "newman-reporter-qase": "^2.1.5" + "newman-reporter-qase": "^2.2.0" } } diff --git a/examples/single/newman/qase.config.json b/examples/single/newman/qase.config.json index ab06d25d..bf0bc38a 100644 --- a/examples/single/newman/qase.config.json +++ b/examples/single/newman/qase.config.json @@ -1,16 +1,17 @@ { "debug": true, - "testops": { "api": { "token": "api_key" }, - "project": "project_code", - "showPublicReportLink": true, - "run": { "complete": true } + }, + "framework": { + "newman": { + "autoCollectParams": true + } } } diff --git a/examples/single/newman/sample-collection.json b/examples/single/newman/sample-collection.json deleted file mode 100644 index d872300c..00000000 --- a/examples/single/newman/sample-collection.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "info": { - "_postman_id": "549b1242-0882-4fbe-8e6e-aa77b58dceec", - "name": "Example collection", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "description": "A sample collection to demonstrate collections as a set of related requests" - }, - "item": [ - { - "name": "Example folder", - "item": [ - { - "name": "A simple GET request with ids", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "// qase: 222 ", - "pm.test('Status code is 200', function () {", - " pm.response.to.have.status(200);", - "})", - "pm.test('expect response json contain args', function () {", - " pm.expect(pm.response.json().args).to.have.property('source')", - " .and.equal('newman-sample-github-collection')", - "})" - ] - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "https://postman-echo.com/get?source=newman-sample-github-collection", - "protocol": "https", - "host": ["postman-echo", "com"], - "path": ["get"], - "query": [ - { - "key": "source", - "value": "newman-sample-github-collection" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "A simple GET request without ids", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 200', function () {", - " pm.response.to.be.ok;", - "})" - ], - "type": "text/javascript" - } - } - ], - "request": { - "url": "https://postman-echo.com/g", - "method": "GET" - }, - "response": [] - }, - { - "name": "A simple GET request without ids 2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Response time is less than 200ms', function () {", - " pm.expect(pm.response.responseTime).to.be.below(10);", - "})" - ], - "type": "text/javascript" - } - } - ], - "request": { - "url": "https://postman-echo.com/get?source=newman-sample-github-collection", - "method": "GET" - }, - "response": [] - } - ] -} diff --git a/examples/single/playwright/README.md b/examples/single/playwright/README.md index bf5baef1..a0305383 100644 --- a/examples/single/playwright/README.md +++ b/examples/single/playwright/README.md @@ -1,7 +1,8 @@ -# Playwright Example +# Playwright Example - E-commerce Test Suite -This is a sample project demonstrating how to write and execute tests using the Playwright framework with integration to -Qase Test Management. +## Overview + +This is a sample project demonstrating realistic e-commerce test scenarios using the Playwright framework with integration to Qase Test Management. The tests run against [saucedemo.com](https://www.saucedemo.com), a demo e-commerce application, showcasing how to structure a real-world test suite with page objects and scenario-based tests that cover authentication, product browsing, cart management, and checkout flows. ## Prerequisites @@ -10,7 +11,7 @@ Ensure that the following tools are installed on your machine: 1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) 2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) -## Setup Instructions +## Installation 1. Clone this repository by running the following commands: ```bash @@ -31,40 +32,73 @@ Ensure that the following tools are installed on your machine: 4. Create a `qase.config.json` file in the root of the project. Follow the instructions on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). -5. To run tests locally without Qase reporting: - ```bash - QASE_MODE=off npx playwright test - ``` +## Configuration -6. To run tests and upload the results to Qase Test Management: - ```bash - QASE_MODE=testops npx playwright test - ``` +The Qase reporter can be configured using environment variables or configuration files. + +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) + +### Using qase.config.json + +Create a `qase.config.json` file in the project root: + +```json +{ + "mode": "testops", + "debug": false, + "testops": { + "api": { + "token": "your_api_token_here" + }, + "project": "YOUR_PROJECT_CODE", + "run": { + "title": "Playwright E-commerce Test Run", + "complete": true + } + } +} +``` + +### Using playwright.config.js + +The project includes a `playwright.config.js` with the Qase reporter configured: + +```javascript +const config = { + timeout: 30000, + testDir: './test', + use: { + baseURL: 'https://www.saucedemo.com', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + reporter: [ + ['list'], + ['playwright-qase-reporter', { /* options */ }], + ], +}; +``` -## Example Files +## Running Tests -This project contains several test files demonstrating different Qase features: +```bash +# Run tests without Qase reporting (default) +npm test -| File | Feature | Description | -|------|---------|-------------| -| `id.spec.js` | Test case linking | Links test to Qase test case by ID using `qase(id, name)` wrapper or `qase.id()` method | -| `title.spec.js` | Custom titles | Sets custom test result titles with `qase.title()` | -| `fields.spec.js` | Custom fields | Sets severity, priority, description, and other metadata with `qase.fields()` | -| `suite.spec.js` | Suite organization | Groups tests into suites and sub-suites with `qase.suite()` | -| `steps.spec.js` | Test steps | Defines execution steps with `await test.step()` (Playwright native) | -| `chain.spec.js` | Method chaining | Demonstrates chaining multiple Qase methods | -| `attach.spec.js` | Attachments | Attaches files and content to test results with `qase.attach()` | -| `comment.spec.js` | Comments | Adds comments to test results with `qase.comment()` | -| `ignore.spec.js` | Ignoring tests | Excludes tests from Qase reporting with `qase.ignore()` | -| `params.spec.js` | Parameters | Reports parameterized test data with `qase.parameters()` | +# Run tests with Qase reporting +QASE_MODE=testops npm test +``` -## Expected Behavior +### Expected Behavior -### Running with QASE_MODE=off (Local Development) +**Running with QASE_MODE=off (Local Development)** When running tests with `QASE_MODE=off`, tests execute normally without Qase reporting: -- Tests run and pass/fail as usual +- Tests run against saucedemo.com and pass/fail as usual - No data is sent to Qase TestOps - No Qase API token required - Output shows standard Playwright test results @@ -72,7 +106,7 @@ When running tests with `QASE_MODE=off`, tests execute normally without Qase rep This mode is useful for local development and debugging. -### Running with QASE_MODE=testops (CI/CD and Reporting) +**Running with QASE_MODE=testops (CI/CD and Reporting)** When running tests with `QASE_MODE=testops`, test results are reported to Qase: @@ -81,73 +115,60 @@ When running tests with `QASE_MODE=testops`, test results are reported to Qase: - Test results include all metadata (steps, attachments, fields, etc.) - Console output includes Qase test run link - Requires valid `QASE_TESTOPS_API_TOKEN` and `QASE_TESTOPS_PROJECT` configuration -- Playwright's native screenshots and traces can be attached automatically +- Screenshots and other attachments are uploaded to Qase -**Steps Example (`steps.spec.js`):** -- Uses Playwright's native `test.step()` which is automatically reported to Qase -- Each step shows execution status, duration, and any errors -- Nested steps appear hierarchically in Qase -- Steps are also visible in Playwright's trace viewer +## Test Scenarios -**Attachments Example (`attach.spec.js`):** -- Files attached via `paths` option appear in test results -- Screenshots captured with `page.screenshot()` can be attached -- Content attached via `content` option is uploaded to Qase -- Attachments are visible in the test run details -- Supports text, JSON, images, videos, and binary files +This project contains four test files demonstrating different e-commerce user flows: -**Multi-Project Support:** -- When configured for multi-project reporting, same test results are sent to multiple Qase projects -- Each project can have different test case IDs for the same test +| File | Scenario | Description | +|------|----------|-------------| +| `login.spec.js` | User Authentication | Tests successful login, invalid password handling, and locked user scenarios | +| `inventory.spec.js` | Product Browsing | Tests product listing, sorting, and detail page navigation | +| `cart.spec.js` | Shopping Cart | Tests adding/removing items and managing multiple products in cart | +| `checkout.spec.js` | Checkout Process | Tests complete checkout flow, validation, and cancellation | -## Configuration +## Qase Features Demonstrated -Example `qase.config.json`: +This example demonstrates all key Qase reporter features in realistic test scenarios: -```json -{ - "mode": "testops", - "debug": false, - "testops": { - "api": { - "token": "your_api_token_here" - }, - "project": "YOUR_PROJECT_CODE", - "run": { - "title": "Playwright Automated Test Run", - "complete": true - } - } -} -``` +| Feature | Files | Description | +|---------|-------|-------------| +| **Test Case Linking** (`qase(id, name)`) | All test files | Links tests to Qase test cases using the wrapper pattern | +| **Test Fields** (`qase.fields()`) | All test files | Sets severity, priority, layer, and other metadata | +| **Test Suites** (`qase.suite()`) | All test files | Organizes tests into hierarchical suites using tab separator | +| **Test Steps** (`test.step()`) | All test files | Uses Playwright native steps for structured test execution | +| **Attachments** (`qase.attach()`) | login.spec.js, inventory.spec.js, cart.spec.js, checkout.spec.js | Attaches screenshots, JSON data, and text files to test results | +| **Comments** (`qase.comment()`) | login.spec.js, inventory.spec.js | Adds contextual comments to test results | +| **Parameters** (`qase.parameters()`) | login.spec.js, cart.spec.js, checkout.spec.js | Reports parameterized test data | +| **Ignore Tests** (`qase.ignore()`) | checkout.spec.js | Excludes specific tests from Qase reporting | -Or configure via `playwright.config.ts`: +## Playwright-Specific Patterns -```typescript -import { defineConfig } from '@playwright/test'; +This example uses Playwright-specific patterns for the Qase reporter: -export default defineConfig({ - reporter: [ - ['list'], - [ - 'playwright-qase-reporter', - { - mode: 'testops', - testops: { - api: { - token: process.env.QASE_TESTOPS_API_TOKEN, - }, - project: 'YOUR_PROJECT_CODE', - run: { - complete: true, - }, - }, - }, - ], - ], -}); +- **Native Steps**: Uses Playwright's native `test.step()` (not `qase.step()`) +- **Attachment Content Type**: Uses `contentType` parameter (not `type`) +- **Test ID Wrapper**: Uses `qase(id, 'name')` wrapper pattern (not mixing with `qase.id()`) +- **Suite Hierarchy**: Uses tab character `\t` as separator in `qase.suite()` + +## Project Structure + +``` +test/ +├── pages/ +│ ├── LoginPage.js # Login page interactions +│ ├── InventoryPage.js # Product listing page interactions +│ ├── CartPage.js # Shopping cart page interactions +│ └── CheckoutPage.js # Checkout flow interactions +├── login.spec.js # Authentication test scenarios +├── inventory.spec.js # Product browsing test scenarios +├── cart.spec.js # Shopping cart test scenarios +└── checkout.spec.js # Checkout test scenarios ``` +Each page object encapsulates the selectors and methods for interacting with a specific page, making tests more maintainable and readable. + ## Additional Resources For more details on how to use this integration with Qase Test Management, visit diff --git a/examples/single/playwright/package.json b/examples/single/playwright/package.json index 33fd3a81..9f1f218d 100644 --- a/examples/single/playwright/package.json +++ b/examples/single/playwright/package.json @@ -2,7 +2,7 @@ "name": "examples-playwright", "private": true, "scripts": { - "test": "QASE_MODE=testops npx playwright test" + "test": "QASE_MODE=${QASE_MODE:-off} npx playwright test" }, "devDependencies": { "@playwright/test": "^1.56.1", diff --git a/examples/single/playwright/playwright.config.js b/examples/single/playwright/playwright.config.js index 0b1260fe..e3a7aa8a 100644 --- a/examples/single/playwright/playwright.config.js +++ b/examples/single/playwright/playwright.config.js @@ -1,5 +1,8 @@ const config = { + timeout: 30000, + testDir: './test', use: { + baseURL: 'https://www.saucedemo.com', screenshot: 'only-on-failure', video: 'retain-on-failure', }, diff --git a/examples/single/playwright/test/attach.spec.js b/examples/single/playwright/test/attach.spec.js deleted file mode 100644 index f79f7b99..00000000 --- a/examples/single/playwright/test/attach.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: attach.spec.js", () => { - test("Test result with attachment", async () => { - // To attach a single file - qase.attach({ paths: "./test/attachments/test-file.txt" }); - - /* - // Add multiple attachments. - qase.attach({ paths: ['/path/to/file', '/path/to/another/file'] }); - - // Upload file's contents directly from code. - qase.attach({ name: 'attachment.txt', content: 'Hello, world!', contentType: 'text/plain' }); - */ - - expect(true).toBe(true); - }); -}); diff --git a/examples/single/playwright/test/attachments/test-file.txt b/examples/single/playwright/test/attachments/test-file.txt deleted file mode 100644 index bcf81250..00000000 Binary files a/examples/single/playwright/test/attachments/test-file.txt and /dev/null differ diff --git a/examples/single/playwright/test/cart.spec.js b/examples/single/playwright/test/cart.spec.js new file mode 100644 index 00000000..40c2b202 --- /dev/null +++ b/examples/single/playwright/test/cart.spec.js @@ -0,0 +1,95 @@ +const { test, expect } = require('@playwright/test'); +const { qase } = require('playwright-qase-reporter'); +const LoginPage = require('./pages/LoginPage'); +const InventoryPage = require('./pages/InventoryPage'); +const CartPage = require('./pages/CartPage'); + +test.describe('Shopping Cart', () => { + let loginPage; + let inventoryPage; + let cartPage; + + test.beforeEach(async ({ page }) => { + loginPage = new LoginPage(page); + inventoryPage = new InventoryPage(page); + cartPage = new CartPage(page); + + await loginPage.goto(); + await loginPage.login('standard_user', 'secret_sauce'); + await expect(page).toHaveURL(/.*inventory.html/); + }); + + test(qase(7, 'User can add product to cart'), async ({ page }) => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tShopping Cart\tAdd Items'); + qase.parameters({ product: 'Sauce Labs Backpack' }); + + await test.step('Add product to cart', async () => { + await inventoryPage.addToCart('sauce-labs-backpack'); + }); + + await test.step('Verify cart badge shows 1 item', async () => { + const cartBadge = await page.locator(inventoryPage.cartBadge).textContent(); + expect(cartBadge).toBe('1'); + }); + + await test.step('Navigate to cart and verify product', async () => { + await inventoryPage.goToCart(); + await expect(page).toHaveURL(/.*cart.html/); + + const itemCount = await cartPage.getItemCount(); + expect(itemCount).toBe(1); + + const cartState = { itemsInCart: 1, product: 'Sauce Labs Backpack' }; + qase.attach({ + name: 'cart-state.json', + content: JSON.stringify(cartState, null, 2), + contentType: 'application/json' + }); + }); + }); + + test(qase(8, 'User can remove product from cart'), async ({ page }) => { + qase.fields({ severity: 'normal', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tShopping Cart\tRemove Items'); + + await test.step('Add product to cart', async () => { + await inventoryPage.addToCart('sauce-labs-backpack'); + }); + + await test.step('Navigate to cart', async () => { + await inventoryPage.goToCart(); + await expect(page).toHaveURL(/.*cart.html/); + }); + + await test.step('Remove product from cart', async () => { + await cartPage.removeItem('sauce-labs-backpack'); + }); + + await test.step('Verify cart is empty', async () => { + const itemCount = await cartPage.getItemCount(); + expect(itemCount).toBe(0); + + const cartBadge = page.locator(inventoryPage.cartBadge); + await expect(cartBadge).not.toBeVisible(); + }); + }); + + test(qase(9, 'User can add multiple products to cart'), async ({ page }) => { + qase.fields({ severity: 'major', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tShopping Cart\tMultiple Items'); + + await test.step('Add first product', async () => { + await inventoryPage.addToCart('sauce-labs-backpack'); + }); + + await test.step('Add second product', async () => { + await inventoryPage.addToCart('sauce-labs-bike-light'); + }); + + await test.step('Verify cart badge shows 2 items', async () => { + const cartBadge = await page.locator(inventoryPage.cartBadge).textContent(); + expect(cartBadge).toBe('2'); + }); + }); +}); diff --git a/examples/single/playwright/test/chain.spec.js b/examples/single/playwright/test/chain.spec.js deleted file mode 100644 index 75f14b92..00000000 --- a/examples/single/playwright/test/chain.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: chain.spec.js", () => { - test("Maintain your test meta-data from code", async () => { - qase - .title("Use qase annotation in a chain") - .fields({ - severity: "critical", - priority: "medium", - layer: "api", - description: `Code it quick, fix it slow, - Tech debt grows where shortcuts go, - Refactor later? Ha! We know.`, - }) - .attach({ paths: "./test/attachments/test-file.txt" }) - .comment( - "This comment will be displayed in the 'Actual Result' field of the test result in Qase.", - ); - }); -}); diff --git a/examples/single/playwright/test/checkout.spec.js b/examples/single/playwright/test/checkout.spec.js new file mode 100644 index 00000000..6e7800ee --- /dev/null +++ b/examples/single/playwright/test/checkout.spec.js @@ -0,0 +1,100 @@ +const { test, expect } = require('@playwright/test'); +const { qase } = require('playwright-qase-reporter'); +const LoginPage = require('./pages/LoginPage'); +const InventoryPage = require('./pages/InventoryPage'); +const CartPage = require('./pages/CartPage'); +const CheckoutPage = require('./pages/CheckoutPage'); + +test.describe('Checkout Process', () => { + let loginPage; + let inventoryPage; + let cartPage; + let checkoutPage; + + test.beforeEach(async ({ page }) => { + loginPage = new LoginPage(page); + inventoryPage = new InventoryPage(page); + cartPage = new CartPage(page); + checkoutPage = new CheckoutPage(page); + + await loginPage.goto(); + await loginPage.login('standard_user', 'secret_sauce'); + await expect(page).toHaveURL(/.*inventory.html/); + + await inventoryPage.addToCart('sauce-labs-backpack'); + await inventoryPage.goToCart(); + await cartPage.checkout(); + }); + + test(qase(10, 'User can complete checkout with valid information'), async ({ page }) => { + qase.fields({ severity: 'critical', priority: 'low', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete Flow'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + await test.step('Fill checkout information', async () => { + await test.step('Enter customer details', async () => { + await checkoutPage.fillInfo('John', 'Doe', '12345'); + }); + + await test.step('Continue to overview', async () => { + await checkoutPage.continue(); + await expect(page).toHaveURL(/.*checkout-step-two.html/); + }); + }); + + await test.step('Complete the order', async () => { + await checkoutPage.finish(); + await expect(page).toHaveURL(/.*checkout-complete.html/); + }); + + await test.step('Verify order completion', async () => { + const completeMessage = await checkoutPage.getCompleteMessage(); + expect(completeMessage).toContain('Thank you for your order'); + qase.attach({ + name: 'order-complete.txt', + content: completeMessage, + contentType: 'text/plain' + }); + }); + }); + + test(qase(11, 'Checkout fails without required information'), async ({ page }) => { + qase.fields({ severity: 'normal', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tValidation'); + qase.parameters({ scenario: 'missing_first_name' }); + + await test.step('Attempt to continue without filling first name', async () => { + await checkoutPage.continue(); + }); + + await test.step('Verify error message is displayed', async () => { + const errorMessage = await page.locator(checkoutPage.errorMessage).textContent(); + expect(errorMessage).toContain('Error: First Name is required'); + }); + }); + + test(qase(12, 'User can cancel checkout'), async ({ page }) => { + qase.fields({ severity: 'major', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tNavigation'); + + await test.step('Click cancel button', async () => { + await page.click(checkoutPage.cancelButton); + }); + + await test.step('Verify return to cart page', async () => { + await expect(page).toHaveURL(/.*cart.html/); + const title = await page.locator(cartPage.pageTitle).textContent(); + expect(title).toBe('Your Cart'); + }); + }); + + test(qase(13, 'Guest checkout (not implemented)'), async ({ page }) => { + qase.ignore(); + qase.fields({ severity: 'major', priority: 'low', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tGuest Flow'); + qase.comment('This test is ignored as guest checkout feature is not yet implemented in the demo app'); + + // Placeholder test for future guest checkout functionality + expect(true).toBe(true); + }); +}); diff --git a/examples/single/playwright/test/comment.spec.js b/examples/single/playwright/test/comment.spec.js deleted file mode 100644 index 7b67c671..00000000 --- a/examples/single/playwright/test/comment.spec.js +++ /dev/null @@ -1,16 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: comment.spec.js", () => { - test("A test case with qase.comment()", () => { - /* - * Please note, this comment is added to a Result, not to the Test case. - */ - - qase.comment( - "This comment will be displayed in the 'Actual Result' field of the test result in Qase.", - ); - - expect(true).toBe(true); - }); -}); diff --git a/examples/single/playwright/test/fields.spec.js b/examples/single/playwright/test/fields.spec.js deleted file mode 100644 index 936ee327..00000000 --- a/examples/single/playwright/test/fields.spec.js +++ /dev/null @@ -1,91 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; -import { markdownContent } from "./markdownContent"; - -test.describe("Example: fields.spec.js\tTest cases with field: Priority", () => { - /* - * Meta data such as Priority, Severity, Layer fields, Description, and pre-conditions can be updated from code. - * This enables you to manage test cases from code directly. - */ - - test("Priority = low", () => { - qase.fields({ priority: "low" }); - expect(true).toBe(true); - }); - - test("Priority = medium", () => { - qase.fields({ priority: "medium" }); - expect(true).toBe(true); - }); - - test("Priority = high", () => { - qase.fields({ priority: "high" }); - expect(true).toBe(true); - }); -}); - -test.describe("Example: fields.spec.js\tTest cases with field: Severity", () => { - test("Severity = trivial", () => { - qase.fields({ severity: "trivial" }); - expect(true).toBe(true); - }); - - test("Severity = minor", () => { - qase.fields({ severity: "minor" }); - expect(true).toBe(true); - }); - - test("Severity = normal", () => { - qase.fields({ severity: "normal" }); - expect(true).toBe(true); - }); - - test("Severity = major", () => { - qase.fields({ severity: "major" }); - expect(true).toBe(true); - }); - - test("Severity = critical", () => { - qase.fields({ severity: "critical" }); - expect(true).toBe(true); - }); - - test("Severity = blocker", () => { - qase.fields({ severity: "blocker" }); - expect(true).toBe(true); - }); -}); - -test.describe("Example: fields.spec.js\tTest cases with field: Layer", () => { - test("Layer = e2e", () => { - qase.fields({ layer: "e2e" }); - expect(true).toBe(true); - }); - - test("Layer = api", () => { - qase.fields({ layer: "api" }); - expect(true).toBe(true); - }); - - test("Layer = unit", () => { - qase.fields({ layer: "unit" }); - expect(true).toBe(true); - }); -}); - -test.describe("Example: fields.spec.js\tTest cases with Description, Pre & Post Conditions", () => { - test("Description with Markdown Support", () => { - qase.fields({ description: markdownContent }); - expect(true).toBe(true); - }); - - test("Preconditions with Markdown Support", () => { - qase.fields({ preconditions: markdownContent }); - expect(true).toBe(true); - }); - - test("Postconditions with Markdown Support", () => { - qase.fields({ postconditions: markdownContent }); - expect(true).toBe(true); - }); -}); diff --git a/examples/single/playwright/test/id.spec.js b/examples/single/playwright/test/id.spec.js deleted file mode 100644 index 0fa43954..00000000 --- a/examples/single/playwright/test/id.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - - -test.describe("Example: id.spec.js", () => { - - test(qase(1, "Defining Id: Format 1"), () => { - expect(true).toBe(true); - }); - - - test("Defining Id: Format 2", () => { - qase.id(2); - expect(true).toBe(true); - }); - - - test( - "Defining Id: Format 3", - { - annotation: { type: "QaseID", description: "3" }, - }, - async () => { - expect(true).toBe(true); - }, - ); -}); diff --git a/examples/single/playwright/test/ignore.spec.js b/examples/single/playwright/test/ignore.spec.js deleted file mode 100644 index d1fba9fd..00000000 --- a/examples/single/playwright/test/ignore.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: ignore.spec.js", () => { - test("This test is executed using Playwright; however, it is NOT reported to Qase", () => { - qase.ignore(); - expect(true).toBe(true); - }); -}); diff --git a/examples/single/playwright/test/inventory.spec.js b/examples/single/playwright/test/inventory.spec.js new file mode 100644 index 00000000..4a484e9b --- /dev/null +++ b/examples/single/playwright/test/inventory.spec.js @@ -0,0 +1,79 @@ +const { test, expect } = require('@playwright/test'); +const { qase } = require('playwright-qase-reporter'); +const LoginPage = require('./pages/LoginPage'); +const InventoryPage = require('./pages/InventoryPage'); + +test.describe('Product Inventory', () => { + let loginPage; + let inventoryPage; + + test.beforeEach(async ({ page }) => { + loginPage = new LoginPage(page); + inventoryPage = new InventoryPage(page); + + await loginPage.goto(); + await loginPage.login('standard_user', 'secret_sauce'); + await expect(page).toHaveURL(/.*inventory.html/); + }); + + test(qase(4, 'User can browse all products'), async ({ page }) => { + qase.fields({ severity: 'normal', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tInventory\tBrowsing'); + + await test.step('Verify inventory page title', async () => { + const title = await page.locator(inventoryPage.pageTitle).textContent(); + expect(title).toBe('Products'); + }); + + await test.step('Count available products', async () => { + const itemCount = await inventoryPage.getItemCount(); + expect(itemCount).toBe(6); + qase.comment(`Found ${itemCount} products available in the inventory`); + }); + + await test.step('Verify product details are visible', async () => { + const productCount = { total: 6, withPrices: 6, withNames: 6 }; + qase.attach({ + name: 'product-count.json', + content: JSON.stringify(productCount, null, 2), + contentType: 'application/json' + }); + }); + }); + + test(qase(5, 'User can sort products by price'), async ({ page }) => { + qase.fields({ severity: 'minor', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tInventory\tSorting'); + qase.parameters({ sortOption: 'lohi' }); + + await test.step('Select sort by price (low to high)', async () => { + await inventoryPage.sortBy('lohi'); + }); + + await test.step('Verify products are sorted correctly', async () => { + const prices = await page.locator(inventoryPage.itemPrice).allTextContents(); + const numericPrices = prices.map(p => parseFloat(p.replace('$', ''))); + + const isSorted = numericPrices.every((price, i) => { + return i === 0 || numericPrices[i - 1] <= price; + }); + + expect(isSorted).toBe(true); + }); + }); + + test(qase(6, 'User can view product details'), async ({ page }) => { + qase.fields({ severity: 'major', priority: 'low', layer: 'e2e' }); + qase.suite('E-commerce\tInventory\tProduct Details'); + + await test.step('Click on first product', async () => { + await page.locator(inventoryPage.itemName).first().click(); + }); + + await test.step('Verify product detail page is displayed', async () => { + await expect(page).toHaveURL(/.*inventory-item.html/); + const backButton = page.locator('[data-test="back-to-products"]'); + await expect(backButton).toBeVisible(); + }); + }); +}); diff --git a/examples/single/playwright/test/login.spec.js b/examples/single/playwright/test/login.spec.js new file mode 100644 index 00000000..0a600c99 --- /dev/null +++ b/examples/single/playwright/test/login.spec.js @@ -0,0 +1,62 @@ +const { test, expect } = require('@playwright/test'); +const { qase } = require('playwright-qase-reporter'); +const LoginPage = require('./pages/LoginPage'); + +test.describe('Authentication', () => { + let loginPage; + + test.beforeEach(async ({ page }) => { + loginPage = new LoginPage(page); + await loginPage.goto(); + }); + + test(qase(1, 'User can login with valid credentials'), async ({ page }) => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + + await test.step('Navigate to login page', async () => { + await expect(page).toHaveURL('https://www.saucedemo.com/'); + }); + + await test.step('Fill in credentials and submit', async () => { + await loginPage.login('standard_user', 'secret_sauce'); + qase.comment('Using standard_user credentials for successful login test'); + }); + + await test.step('Verify successful login', async () => { + await expect(page).toHaveURL(/.*inventory.html/); + const screenshot = await page.screenshot({ encoding: 'base64' }); + qase.attach({ name: 'login-success.png', content: screenshot, contentType: 'image/png' }); + }); + }); + + test(qase(2, 'User cannot login with invalid password'), async ({ page }) => { + qase.fields({ severity: 'minor', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'standard_user', password: 'wrong_password' }); + + await test.step('Attempt login with invalid password', async () => { + await loginPage.login('standard_user', 'wrong_password'); + }); + + await test.step('Verify error message is displayed', async () => { + const errorText = await loginPage.getErrorText(); + expect(errorText).toContain('Username and password do not match'); + }); + }); + + test(qase(3, 'Locked user cannot login'), async ({ page }) => { + qase.fields({ severity: 'major', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'locked_out_user' }); + + await test.step('Attempt login with locked user', async () => { + await loginPage.login('locked_out_user', 'secret_sauce'); + }); + + await test.step('Verify locked-out error message', async () => { + const errorText = await loginPage.getErrorText(); + expect(errorText).toContain('Sorry, this user has been locked out'); + }); + }); +}); diff --git a/examples/single/playwright/test/markdownContent.js b/examples/single/playwright/test/markdownContent.js deleted file mode 100644 index 3f3583c6..00000000 --- a/examples/single/playwright/test/markdownContent.js +++ /dev/null @@ -1,111 +0,0 @@ -export const markdownContent = `# Markdown Syntax Showcase - -## Headers -### Different Header Levels -#### Are Supported -##### In Markdown -###### Even Smallest Headers - -
- -## Text Formatting -*Italic Text* -**Bold Text** -***Bold and Italic*** -~~Strikethrough Text~~ - -
- -## Lists -### Unordered Lists -- First item -- Second item - * Nested item - * Another nested item - -
- -### Ordered Lists -1. First ordered item -2. Second ordered item - 1. Nested ordered item - 2. Another nested ordered item - -
- -# # Links -[Inline Link](https://www.example.com) -[Link with Title](https://www.example.com "Website Title") - -[Reference-style Link][Reference] -[Reference]: https://www.example.com - -
- -## Code -### Inline Code -Here is some \`inline code\` - -### Code Blocks -\`\`\`javascript -function exampleCode() { - return "Code blocks are supported"; -} -\`\`\` - -\`\`\`python -def python_example(): - return "Multiple language syntax highlighting" -\`\`\` - -
- -## Blockquotes -> This is a blockquote -> -> It can span multiple lines -> -> ### Even with Headers Inside -> -> - And lists -> - Are possible - -
- -## Horizontal Rules ---- -*** -___ - -
- -## Tables -| Column 1 | Column 2 | Column 3 | -|----------|----------|----------| -| Row 1, Col 1 | Row 1, Col 2 | Row 1, Col 3 | -| Row 2, Col 1 | Row 2, Col 2 | Row 2, Col 3 | - -
- -## Task Lists -- [x] Completed task -- [ ] Incomplete task -- [ ] Another incomplete task - -
- -## Footnotes -Here's a sentence with a footnote[^1]. - -[^1]: This is the footnote content. - -
- -## HTML Inline Elements -Some underlined and superscript text. - -
- -## Escaping Characters -\\*This is not italicized\\* -\\# This is a literal hash`; diff --git a/examples/single/playwright/test/pages/CartPage.js b/examples/single/playwright/test/pages/CartPage.js new file mode 100644 index 00000000..f7191985 --- /dev/null +++ b/examples/single/playwright/test/pages/CartPage.js @@ -0,0 +1,29 @@ +class CartPage { + constructor(page) { + this.page = page; + this.cartItems = '.cart_item'; + this.itemName = '[data-test="inventory-item-name"]'; + this.itemPrice = '[data-test="inventory-item-price"]'; + this.checkoutButton = '[data-test="checkout"]'; + this.continueShoppingButton = '[data-test="continue-shopping"]'; + this.pageTitle = '.title'; + } + + async removeItem(productSlug) { + await this.page.click(`[data-test="remove-${productSlug}"]`); + } + + async getItemCount() { + return await this.page.locator(this.cartItems).count(); + } + + async checkout() { + await this.page.click(this.checkoutButton); + } + + async continueShopping() { + await this.page.click(this.continueShoppingButton); + } +} + +module.exports = CartPage; diff --git a/examples/single/playwright/test/pages/CheckoutPage.js b/examples/single/playwright/test/pages/CheckoutPage.js new file mode 100644 index 00000000..44a0f866 --- /dev/null +++ b/examples/single/playwright/test/pages/CheckoutPage.js @@ -0,0 +1,35 @@ +class CheckoutPage { + constructor(page) { + this.page = page; + this.firstName = '[data-test="firstName"]'; + this.lastName = '[data-test="lastName"]'; + this.postalCode = '[data-test="postalCode"]'; + this.continueButton = '[data-test="continue"]'; + this.cancelButton = '[data-test="cancel"]'; + this.finishButton = '[data-test="finish"]'; + this.completeHeader = '.complete-header'; + this.backToProducts = '[data-test="back-to-products"]'; + this.errorMessage = '[data-test="error"]'; + this.pageTitle = '.title'; + } + + async fillInfo(first, last, zip) { + await this.page.fill(this.firstName, first); + await this.page.fill(this.lastName, last); + await this.page.fill(this.postalCode, zip); + } + + async continue() { + await this.page.click(this.continueButton); + } + + async finish() { + await this.page.click(this.finishButton); + } + + async getCompleteMessage() { + return await this.page.textContent(this.completeHeader); + } +} + +module.exports = CheckoutPage; diff --git a/examples/single/playwright/test/pages/InventoryPage.js b/examples/single/playwright/test/pages/InventoryPage.js new file mode 100644 index 00000000..cb992114 --- /dev/null +++ b/examples/single/playwright/test/pages/InventoryPage.js @@ -0,0 +1,34 @@ +class InventoryPage { + constructor(page) { + this.page = page; + this.inventoryItems = '.inventory_item'; + this.itemName = '[data-test="inventory-item-name"]'; + this.itemPrice = '[data-test="inventory-item-price"]'; + this.sortDropdown = '.product_sort_container'; + this.cartBadge = '.shopping_cart_badge'; + this.cartLink = '#shopping_cart_container a'; + this.pageTitle = '.title'; + } + + async addToCart(productSlug) { + await this.page.click(`[data-test="add-to-cart-${productSlug}"]`); + } + + async removeFromCart(productSlug) { + await this.page.click(`[data-test="remove-${productSlug}"]`); + } + + async getItemCount() { + return await this.page.locator(this.inventoryItems).count(); + } + + async sortBy(optionValue) { + await this.page.selectOption(this.sortDropdown, optionValue); + } + + async goToCart() { + await this.page.click(this.cartLink); + } +} + +module.exports = InventoryPage; diff --git a/examples/single/playwright/test/pages/LoginPage.js b/examples/single/playwright/test/pages/LoginPage.js new file mode 100644 index 00000000..831ca190 --- /dev/null +++ b/examples/single/playwright/test/pages/LoginPage.js @@ -0,0 +1,25 @@ +class LoginPage { + constructor(page) { + this.page = page; + this.usernameInput = '[data-test="username"]'; + this.passwordInput = '[data-test="password"]'; + this.loginButton = '[data-test="login-button"]'; + this.errorMessage = '[data-test="error"]'; + } + + async goto() { + await this.page.goto('https://www.saucedemo.com'); + } + + async login(username, password) { + await this.page.fill(this.usernameInput, username); + await this.page.fill(this.passwordInput, password); + await this.page.click(this.loginButton); + } + + async getErrorText() { + return await this.page.textContent(this.errorMessage); + } +} + +module.exports = LoginPage; diff --git a/examples/single/playwright/test/params.spec.js b/examples/single/playwright/test/params.spec.js deleted file mode 100644 index cc8d036a..00000000 --- a/examples/single/playwright/test/params.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -const testCases = [ - { browser: "Chromium", username: "@alice", password: "123" }, - { browser: "Firefox", username: "@bob", password: "456" }, - { browser: "Webkit", username: "@charlie", password: "789" }, -]; - -test.describe("Example param.cy.js\tSingle Parameter", () => { - testCases.forEach(({ browser }) => { - test(`Test login with ${browser}`, () => { - qase.title("Verify if login page loads successfully"); - - /* - * Instead of creating three separate test cases in Qase, this method will add a 'browser' parameter, with three values. - */ - - qase.parameters({ Browser: browser }); - - expect(true).toBe(true); - }); - }); -}); - -test.describe("Example param.cy.js\tGroup Parameter", () => { - testCases.forEach(({ username, password }) => { - test(`Test login with ${username} using qase.groupParameters`, () => { - qase.title("Verify if user is able to login with their username."); - - /* - * Here, we're grouping the username and password parameters to track them together, as a set of parameters for the test. - * This will show the username and password combinations for the test. - */ - - qase.groupParameters({ - Username: username, - Password: password, - }); - - expect(true).toBe(true); - }); - }); -}); diff --git a/examples/single/playwright/test/steps.spec.js b/examples/single/playwright/test/steps.spec.js deleted file mode 100644 index 7fdc792d..00000000 --- a/examples/single/playwright/test/steps.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: steps.spec.js", () => { - test("A Test case with steps, updated from code", async () => { - await test.step("Initialize the environment", async () => { - // Set up test environment - }); - - await test.step("Test Core Functionality of the app", async () => { - // Exercise core functionality - }); - - await test.step("Verify Expected Behavior of the app", async () => { - // Assert expected behavior - }); - - await test.step("Verify if user is able to log out successfully", async () => { - // Expected user to be logged out (but, ran into a problem!). - expect(true).toBe(true); - }); - }); -}); diff --git a/examples/single/playwright/test/suite.spec.js b/examples/single/playwright/test/suite.spec.js deleted file mode 100644 index 34a3914f..00000000 --- a/examples/single/playwright/test/suite.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: suite.spec.js", () => { - test("Test with a defined suite", () => { - qase.suite("Example: suite.spec.js\tThis shall be a suite name"); - expect(true).toBe(true); - }); - - test("Test within multiple levels of suite", () => { - qase.suite( - "Example: suite.spec.js\tThis shall be a suite name\tChild Suite", - ); - // A `\t` is used for dividing each suite name - expect(true).toBe(true); - }); -}); diff --git a/examples/single/playwright/test/title.spec.js b/examples/single/playwright/test/title.spec.js deleted file mode 100644 index 4bc747b2..00000000 --- a/examples/single/playwright/test/title.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { qase } from "playwright-qase-reporter"; - -test.describe("Example: title.spec.js", () => { - test("Test without qase.title() method", () => { - /* - * Here, we're are not using a qase.title() method - * Given, you have "Auto-create cases" option enabled for this project. - * A new test will be created in Qase, with the test's title. - */ - - expect(true).toBe(true); - }); - - test("This won't appear in Qase", () => { - qase.title("This text will be the title of the test, in Qase"); - - /* - * Here, the Qase Test case's title will be taken from qase.title() method. - */ - - expect(true).toBe(true); - }); -}); - -/* - * - * Q) What about the tests where the qase.title() method is not used? - * => Those test cases will have the "Title of this test" as the newly created case's title. - * - * - * Q) I'm running this test case, but it's not creating any test case in Qase. - * My test run is empty, what am I doing wrong? - * - * => Go to your Qase Project's settings, switch to the Test runs tab. - * Under "Automated Testing" - Enable "Create test cases option" [https://i.imgur.com/PtZPrrY.png] - * - * - * Q) What happens if I change the title in `qase.title()` ? - * => Since, there's no link between the Qase test case and this test, changing the title will lead to - * a new case being created in your Project repository. - * - */ diff --git a/examples/single/testcafe/README.md b/examples/single/testcafe/README.md index d0951cff..de4acc22 100644 --- a/examples/single/testcafe/README.md +++ b/examples/single/testcafe/README.md @@ -1,6 +1,8 @@ -# TestCafe Example +# TestCafe Example - E-commerce Test Suite -This is a sample project demonstrating how to write and execute tests using the TestCafe framework with integration to Qase Test Management. +## Overview + +This is a comprehensive example demonstrating realistic e-commerce testing scenarios using TestCafe with Qase Test Management integration. Tests are executed against [saucedemo.com](https://www.saucedemo.com), a demo e-commerce application, covering authentication, product inventory, shopping cart, and checkout flow. All tests demonstrate Qase reporter integration using TestCafe's unique builder pattern and API. ## Prerequisites @@ -8,75 +10,272 @@ Ensure that the following tools are installed on your machine: 1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) 2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) +3. Chrome browser (or modify package.json to use a different browser) -## Setup Instructions +## Installation -1. Clone this repository by running the following commands: +1. Clone this repository: ```bash git clone https://github.com/qase-tms/qase-javascript.git cd qase-javascript/examples/single/testcafe ``` -2. Install the project dependencies: +2. Install dependencies: ```bash npm install ``` -3. Create a `qase.config.json` file in the root of the project. Follow the instructions on [how to configure the file](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration). +3. Configure Qase integration by editing `qase.config.json`: + ```json + { + "testops": { + "api": { + "token": "your-api-token-here" + }, + "project": "your-project-code-here", + "uploadAttachments": true, + "run": { + "complete": true + } + } + } + ``` + + See [configuration guide](https://github.com/qase-tms/qase-javascript/tree/main/qase-javascript-commons#configuration) for all options. -## Example Files +## Configuration -This example includes: +The Qase reporter can be configured using environment variables or configuration files. -* **simpleTests.js** — Basic test examples demonstrating: - * Tests with and without Qase metadata - * Builder pattern usage: `test.meta(qase.id(1).create())` - * Setting title, fields, and ignore flags -* **attachmentTests.js** — Attachment examples showing: - * File attachments from paths - * Content attachments from memory using `type` parameter - * Step-level attachments -* **qase.config.json** — Qase reporter configuration +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) -## Running Tests +See the qase.config.json file for additional configuration options. -To run tests locally without reporting to Qase: +## Running Tests ```bash -QASE_MODE=off npm test +# Run tests without Qase reporting (default) +npm test + +# Run tests with Qase reporting +QASE_MODE=testops npm test ``` -To run tests and upload the results to Qase Test Management: +### Run specific test file ```bash -npm test +QASE_MODE=testops npx testcafe chrome tests/login.test.js -r spec,qase ``` -Or with explicit mode and browser: +### Run with different browser ```bash -QASE_MODE=testops npx testcafe chrome simpleTests.js +QASE_MODE=testops npx testcafe firefox tests/*.test.js -r spec,qase ``` -## Expected Behavior +### Expected Behavior + +When tests run with Qase reporting enabled: + +1. **Test results** are uploaded to your Qase project in real-time +2. **Attachments** (JSON, text files) are uploaded to Qase +3. **Steps** appear hierarchically in test case results +4. **Metadata** (fields, suites, parameters) is applied to test cases +5. **Ignored tests** are executed locally but not reported to Qase +6. **Screenshots** of failures are automatically captured and attached + +## Test Scenarios + +| File | Description | Test Count | Scenarios | +|------|-------------|------------|-----------| +| **login.test.js** | Authentication testing | 3 | Valid login, invalid password, locked user | +| **inventory.test.js** | Product browsing and filtering | 3 | Browse products, sort by price, view details | +| **cart.test.js** | Shopping cart operations | 3 | Add item, remove item, multiple items | +| **checkout.test.js** | Purchase completion flow | 4 | Complete checkout, validation errors, cancel, ignored test | + +**Total:** 13 tests covering complete e-commerce user journey + +## Qase Features Demonstrated + +| Feature | Usage Example | Description | +|---------|---------------|-------------| +| **Test ID** | `qase.id(1)` | Link test to existing Qase test case | +| **Title** | `qase.title('Custom title')` | Set custom test title | +| **Fields** | `qase.fields({severity:'critical', priority:'high'})` | Add metadata fields | +| **Suite** | `qase.suite('E-commerce\tCart\tAdd')` | Organize with suite hierarchy (tab-separated) | +| **Parameters** | `qase.parameters({username:'user1'})` | Document test parameters | +| **Steps** | `await qase.step('name', async () => {})` | Create hierarchical test steps | +| **Nested Steps** | `await qase.step('parent', async (s1) => { await s1.step('child', ...) })` | Multi-level step nesting | +| **Attachments** | `qase.attach({name:'file.txt', content:'...', type:'text/plain'})` | Attach files or content | +| **Comments** | `qase.comment('Additional info')` | Add runtime comments | +| **Ignore** | `qase.ignore()` | Skip reporting to Qase | -When tests execute with Qase reporting enabled: +## TestCafe-Specific Patterns + +### 1. Builder Pattern with `.create()` + +**CRITICAL:** Every `test.meta()` call **MUST** end with `.create()`. This is the most common mistake. + +```javascript +// ✅ CORRECT +test.meta(qase.id(1).title('Test').create())('Test name', async t => { + // test code +}); + +// ❌ WRONG - Missing .create() +test.meta(qase.id(1).title('Test'))('Test name', async t => { + // test code +}); +``` + +### 2. Chaining Metadata + +Chain multiple metadata methods before calling `.create()`: + +```javascript +test.meta( + qase.id(1) + .title('Complex test') + .fields({severity: 'high'}) + .suite('Module\tFeature') + .parameters({user: 'admin'}) + .create() +)('Test name', async t => { + // test code +}); +``` + +### 3. Attachment Type Parameter + +Use `type` (not `contentType`) for MIME type: + +```javascript +// ✅ CORRECT +await qase.attach({ + name: 'data.json', + content: JSON.stringify({key: 'value'}), + type: 'application/json' // Use 'type' +}); + +// ❌ WRONG +await qase.attach({ + name: 'data.json', + content: JSON.stringify({key: 'value'}), + contentType: 'application/json' // Don't use 'contentType' +}); +``` -* **Tests with .meta(qase.id().create())** are linked to existing Qase test cases -* **Builder pattern chaining** allows combining metadata: `qase.id(1).title('...').fields({...}).create()` -* **Attachments use 'type' parameter** for MIME type specification (not 'contentType') -* **Steps with nested callbacks** provide hierarchical test organization -* **TestCafe's async/await** syntax works seamlessly with Qase reporter +### 4. Async Steps -## Framework-Specific Features +TestCafe steps are asynchronous and must use `async/await`: -TestCafe with Qase has unique patterns: +```javascript +// ✅ CORRECT +await qase.step('Step name', async () => { + await t.click(someButton); +}); -* **Builder pattern** — Use `.meta(qase.id().create())` to attach metadata to tests -* **Type parameter for attachments** — `qase.attach({ name: 'file.txt', content: '...', type: 'text/plain' })` -* **Nested steps via callbacks** — Use step parameter (s, s1, s2) for nesting: `await qase.step('Parent', async (s1) => { await s1.step('Child', ...) })` -* **No wrapper function** — TestCafe uses `.meta()` API instead of wrapping test declarations +// ✅ CORRECT - Nested steps +await qase.step('Parent step', async (s1) => { + await s1.step('Child step', async () => { + await t.typeText(input, 'text'); + }); +}); +``` + +### 5. Fixture Setup + +Use TestCafe's fixture API for test organization: + +```javascript +fixture`Suite Name` + .page`https://example.com` + .beforeEach(async t => { + // Setup code + }); +``` + +## Project Structure + +``` +testcafe/ +├── tests/ +│ ├── pages/ # Page Object Model +│ │ ├── LoginPage.js # Login page selectors +│ │ ├── InventoryPage.js # Product listing selectors +│ │ ├── CartPage.js # Shopping cart selectors +│ │ └── CheckoutPage.js # Checkout form selectors +│ ├── login.test.js # Authentication scenarios +│ ├── inventory.test.js # Product browsing scenarios +│ ├── cart.test.js # Cart management scenarios +│ └── checkout.test.js # Checkout flow scenarios +├── qase.config.json # Qase reporter configuration +├── package.json +└── README.md +``` + +## Page Object Pattern + +This example uses TestCafe's Selector-based page objects. All page objects: + +- Import `Selector` from TestCafe +- Define selectors as class properties in constructor +- Export as singleton instances +- Use data-test attributes for reliable element location + +**Example:** + +```javascript +import { Selector } from 'testcafe'; + +class LoginPage { + constructor() { + this.usernameInput = Selector('[data-test="username"]'); + this.passwordInput = Selector('[data-test="password"]'); + this.loginButton = Selector('[data-test="login-button"]'); + } +} + +export default new LoginPage(); +``` + +## Credentials + +The example uses demo credentials from saucedemo.com: + +- **Standard user:** username: `standard_user`, password: `secret_sauce` +- **Locked user:** username: `locked_out_user`, password: `secret_sauce` + +These are publicly available demo credentials for testing purposes only. + +## Troubleshooting + +### Common Issues + +1. **Missing `.create()`**: If tests don't report to Qase, ensure all `.meta()` calls end with `.create()` +2. **Wrong attachment parameter**: Use `type` not `contentType` for attachments +3. **Step not awaited**: All `qase.step()` calls must be awaited in TestCafe +4. **Browser not found**: Install Chrome or modify package.json to use a different browser + +### Debug Mode + +Enable debug mode in `qase.config.json` to see detailed logging: + +```json +{ + "debug": true, + "testops": { + // ... rest of config + } +} +``` ## Additional Resources -For more details on how to use this integration with Qase Test Management, visit the [Qase TestCafe documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-testcafe). +- [Qase TestCafe Documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-testcafe) +- [TestCafe Documentation](https://testcafe.io/documentation) +- [Qase Test Management](https://qase.io) +- [SauceDemo Test Site](https://www.saucedemo.com) diff --git a/examples/single/testcafe/attachmentTests.js b/examples/single/testcafe/attachmentTests.js deleted file mode 100644 index e520cd7a..00000000 --- a/examples/single/testcafe/attachmentTests.js +++ /dev/null @@ -1,39 +0,0 @@ -import { test } from 'testcafe'; -import { qase } from 'testcafe-reporter-qase/qase'; - -fixture`Attachment tests` - .page`http://devexpress.github.io/testcafe/example/`; - -test('Test with file attachment success', async (t) => { - qase.attach({ paths: ['examples/testcafe/attachmentTests.js'] }); - await t.expect(true).ok(); -}); - -test('Test with file attachment failed', async (t) => { - qase.attach({ paths: ['examples/testcafe/attachmentTests.js'] }); - await t.expect(false).ok(); -}); - -test('Test with content attachment success', async (t) => { - qase.attach({ name: 'log.txt', content: 'Hello, World!', type: 'text/plain' }); - await t.expect(true).ok(); -}); - -test('Test with content attachment failed', async (t) => { - qase.attach({ name: 'log.txt', content: 'Hello, World!', type: 'text/plain' }); - await t.expect(false).ok(); -}); - -test('Test with step attachment success', async (t) => { - await qase.step('Step with attachment', async (s) => { - s.attach({ name: 'log.txt', content: 'Hello, World!', type: 'text/plain' }); - }); - await t.expect(true).ok(); -}); - -test('Test with step attachment failed', async (t) => { - await qase.step('Step with attachment', async (s) => { - s.attach({ name: 'log.txt', content: 'Hello, World!', type: 'text/plain' }); - }); - await t.expect(false).ok(); -}); diff --git a/examples/single/testcafe/package.json b/examples/single/testcafe/package.json index 6a7a2a75..77c5c3f8 100644 --- a/examples/single/testcafe/package.json +++ b/examples/single/testcafe/package.json @@ -2,7 +2,7 @@ "name": "examples-testcafe", "private": true, "scripts": { - "test": "QASE_MODE=testops npx testcafe \"chrome\" simpleTests.js attachmentTests.js -r spec,qase -s path=screenshots,takeOnFails=true" + "test": "QASE_MODE=${QASE_MODE:-off} npx testcafe chrome tests/*.test.js -r spec,qase -s path=screenshots,takeOnFails=true" }, "devDependencies": { "eslint-plugin-testcafe": "^0.2.1", diff --git a/examples/single/testcafe/simpleTests.js b/examples/single/testcafe/simpleTests.js deleted file mode 100644 index b945d1a0..00000000 --- a/examples/single/testcafe/simpleTests.js +++ /dev/null @@ -1,51 +0,0 @@ -import { test } from 'testcafe'; -import { qase } from 'testcafe-reporter-qase/qase'; - -fixture`Simple tests` - .page`http://devexpress.github.io/testcafe/example/`; - -test('Test without metadata success', async (t) => { - await t.expect(true).ok(); -}); - -test('Test without metadata failed', async (t) => { - await t.expect(false).ok(); -}); - -test.meta(qase.id(1).create())('Test with QaseID success', async t => { - await t.expect(true).ok(); -}); - -test.meta(qase.id(2).create())('Test with QaseID failed', async t => { - await t.expect(false).ok(); -}); - -test.meta(qase.title('Test with title success').create())('Test with title success', async t => { - await t.expect(true).ok(); -}); - -test.meta(qase.title('Test with title failed').create())('Test with title failed', async t => { - await t.expect(false).ok(); -}); - -test.meta(qase.fields({ - 'description': 'Test description', - 'preconditions': 'Some text', -}).create())('Test with fields success', async t => { - await t.expect(true).ok(); -}); - -test.meta(qase.fields({ - 'description': 'Test description', - 'preconditions': 'Some text', -}).create())('Test with fields failed', async t => { - await t.expect(false).ok(); -}); - -test.meta(qase.ignore().create())('Test with ignore success', async t => { - await t.expect(true).ok(); -}); - -test.meta(qase.ignore().create())('Test with ignore failed', async t => { - await t.expect(false).ok(); -}); diff --git a/examples/single/testcafe/tests/cart.test.js b/examples/single/testcafe/tests/cart.test.js new file mode 100644 index 00000000..c96c0e51 --- /dev/null +++ b/examples/single/testcafe/tests/cart.test.js @@ -0,0 +1,112 @@ +import { qase } from 'testcafe-reporter-qase/qase'; +import loginPage from './pages/LoginPage.js'; +import inventoryPage from './pages/InventoryPage.js'; +import cartPage from './pages/CartPage.js'; + +fixture`Cart Management` + .page`https://www.saucedemo.com` + .beforeEach(async t => { + // Login before each cart test + await t + .typeText(loginPage.usernameInput, 'standard_user') + .typeText(loginPage.passwordInput, 'secret_sauce') + .click(loginPage.loginButton); + await t.expect(inventoryPage.pageTitle.innerText).eql('Products'); + }); + +test.meta(qase.id(7).title('Add product to cart').fields({ + severity: 'critical', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tCart\tAdd Items').parameters({ + product: 'Sauce Labs Backpack' +}).create())('Add to cart', async t => { + await qase.step('Add Sauce Labs Backpack to cart', async () => { + await t.click(inventoryPage.addToCartButton('sauce-labs-backpack')); + }); + + await qase.step('Verify cart badge shows 1', async () => { + await t.expect(inventoryPage.cartBadge.innerText).eql('1', 'Cart badge should show 1 item'); + }); + + await qase.step('Navigate to cart', async () => { + await t.click(inventoryPage.cartLink); + }); + + await qase.step('Verify product in cart', async () => { + await t.expect(cartPage.pageTitle.innerText).eql('Your Cart'); + const itemCount = await cartPage.items.count; + await t.expect(itemCount).eql(1, 'Cart should contain 1 item'); + + const itemName = await cartPage.itemName.innerText; + await t.expect(itemName).eql('Sauce Labs Backpack', 'Correct product in cart'); + }); + + await qase.comment('Product successfully added to cart and visible in cart page'); + + await qase.attach({ + name: 'cart-state.txt', + content: 'Product: Sauce Labs Backpack\nQuantity: 1\nStatus: Added', + type: 'text/plain' + }); +}); + +test.meta(qase.id(8).title('Remove product from cart').fields({ + severity: 'high', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tCart\tRemove Items').create())('Remove from cart', async t => { + await qase.step('Add product to cart', async () => { + await t.click(inventoryPage.addToCartButton('sauce-labs-backpack')); + await t.expect(inventoryPage.cartBadge.innerText).eql('1'); + }); + + await qase.step('Navigate to cart', async () => { + await t.click(inventoryPage.cartLink); + }); + + await qase.step('Remove product from cart', async () => { + await t.click(cartPage.removeButton('sauce-labs-backpack')); + }); + + await qase.step('Verify cart is empty', async () => { + const itemCount = await cartPage.items.count; + await t.expect(itemCount).eql(0, 'Cart should be empty'); + await t.expect(inventoryPage.cartBadge.exists).notOk('Cart badge should not be visible'); + }); + + await qase.comment('Product successfully removed from cart'); +}); + +test.meta(qase.id(9).title('Add multiple products to cart').fields({ + severity: 'critical', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tCart\tAdd Items').create())('Add multiple products', async t => { + await qase.step('Add first product', async () => { + await t.click(inventoryPage.addToCartButton('sauce-labs-backpack')); + await t.expect(inventoryPage.cartBadge.innerText).eql('1', 'Cart badge should show 1'); + }); + + await qase.step('Add second product', async () => { + await t.click(inventoryPage.addToCartButton('sauce-labs-bike-light')); + await t.expect(inventoryPage.cartBadge.innerText).eql('2', 'Cart badge should show 2'); + }); + + await qase.step('Navigate to cart and verify', async () => { + await t.click(inventoryPage.cartLink); + const itemCount = await cartPage.items.count; + await t.expect(itemCount).eql(2, 'Cart should contain 2 items'); + }); + + await qase.comment('Multiple products successfully added to cart'); + + await qase.attach({ + name: 'multi-cart.json', + content: JSON.stringify({ + products: ['Sauce Labs Backpack', 'Sauce Labs Bike Light'], + totalItems: 2 + }, null, 2), + type: 'application/json' + }); +}); diff --git a/examples/single/testcafe/tests/checkout.test.js b/examples/single/testcafe/tests/checkout.test.js new file mode 100644 index 00000000..1a88d055 --- /dev/null +++ b/examples/single/testcafe/tests/checkout.test.js @@ -0,0 +1,138 @@ +import { qase } from 'testcafe-reporter-qase/qase'; +import loginPage from './pages/LoginPage.js'; +import inventoryPage from './pages/InventoryPage.js'; +import cartPage from './pages/CartPage.js'; +import checkoutPage from './pages/CheckoutPage.js'; + +fixture`Checkout Flow` + .page`https://www.saucedemo.com` + .beforeEach(async t => { + // Login, add product, navigate to cart, and click checkout + await t + .typeText(loginPage.usernameInput, 'standard_user') + .typeText(loginPage.passwordInput, 'secret_sauce') + .click(loginPage.loginButton); + await t.expect(inventoryPage.pageTitle.innerText).eql('Products'); + + // Add a product + await t.click(inventoryPage.addToCartButton('sauce-labs-backpack')); + + // Go to cart + await t.click(inventoryPage.cartLink); + await t.expect(cartPage.pageTitle.innerText).eql('Your Cart'); + + // Click checkout + await t.click(cartPage.checkoutButton); + await t.expect(checkoutPage.pageTitle.innerText).eql('Checkout: Your Information'); + }); + +test.meta(qase.id(10).title('Complete checkout successfully').fields({ + severity: 'critical', + priority: 'critical', + layer: 'e2e' +}).suite('E-commerce\tCheckout\tComplete').parameters({ + firstName: 'John', + lastName: 'Doe', + postalCode: '12345' +}).create())('Complete checkout', async t => { + await qase.step('Fill checkout information', async (s1) => { + await s1.step('Enter first name', async () => { + await t.typeText(checkoutPage.firstNameInput, 'John'); + }); + + await s1.step('Enter last name', async () => { + await t.typeText(checkoutPage.lastNameInput, 'Doe'); + }); + + await s1.step('Enter postal code', async () => { + await t.typeText(checkoutPage.postalCodeInput, '12345'); + }); + }); + + await qase.step('Continue to overview', async () => { + await t.click(checkoutPage.continueButton); + const title = await checkoutPage.pageTitle.innerText; + await t.expect(title).eql('Checkout: Overview', 'Should be on overview page'); + }); + + await qase.step('Finish checkout', async () => { + await t.click(checkoutPage.finishButton); + }); + + await qase.step('Verify order confirmation', async () => { + const confirmHeader = await checkoutPage.completeHeader.innerText; + await t.expect(confirmHeader).eql('Thank you for your order!', 'Should show confirmation message'); + }); + + await qase.comment('Order successfully completed with valid information'); + + await qase.attach({ + name: 'order-details.json', + content: JSON.stringify({ + customer: { + firstName: 'John', + lastName: 'Doe', + postalCode: '12345' + }, + product: 'Sauce Labs Backpack', + status: 'completed' + }, null, 2), + type: 'application/json' + }); +}); + +test.meta(qase.id(11).title('Checkout validation error').fields({ + severity: 'high', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tCheckout\tValidation').parameters({ + scenario: 'missing_first_name' +}).create())('Missing first name error', async t => { + await qase.step('Leave first name empty', async () => { + // Only fill last name and postal code + await t + .typeText(checkoutPage.lastNameInput, 'Doe') + .typeText(checkoutPage.postalCodeInput, '12345'); + }); + + await qase.step('Try to continue', async () => { + await t.click(checkoutPage.continueButton); + }); + + await qase.step('Verify validation error', async () => { + await t.expect(checkoutPage.errorMessage.exists).ok('Error message should be displayed'); + const errorText = await checkoutPage.errorMessage.innerText; + await t.expect(errorText).contains('First Name is required', 'Error should mention first name'); + }); + + await qase.comment('Validation correctly prevents checkout with missing first name'); +}); + +test.meta(qase.id(12).title('Cancel checkout').fields({ + severity: 'medium', + priority: 'medium', + layer: 'e2e' +}).suite('E-commerce\tCheckout\tCancel').create())('Cancel checkout', async t => { + await qase.step('Click cancel button', async () => { + await t.click(checkoutPage.cancelButton); + }); + + await qase.step('Verify returned to cart', async () => { + await t.expect(cartPage.pageTitle.innerText).eql('Your Cart', 'Should return to cart page'); + }); + + await qase.step('Verify product still in cart', async () => { + const itemCount = await cartPage.items.count; + await t.expect(itemCount).eql(1, 'Product should still be in cart'); + }); + + await qase.comment('Checkout cancelled and returned to cart successfully'); +}); + +test.meta(qase.ignore().create())('Ignored checkout test', async t => { + await qase.step('This test is ignored', async () => { + await t.expect(true).ok('This will not be reported to Qase'); + }); + + await qase.comment('This test demonstrates the ignore functionality'); +}); diff --git a/examples/single/testcafe/tests/inventory.test.js b/examples/single/testcafe/tests/inventory.test.js new file mode 100644 index 00000000..fbbefbbf --- /dev/null +++ b/examples/single/testcafe/tests/inventory.test.js @@ -0,0 +1,104 @@ +import { qase } from 'testcafe-reporter-qase/qase'; +import loginPage from './pages/LoginPage.js'; +import inventoryPage from './pages/InventoryPage.js'; + +fixture`Product Inventory` + .page`https://www.saucedemo.com` + .beforeEach(async t => { + // Login before each inventory test + await t + .typeText(loginPage.usernameInput, 'standard_user') + .typeText(loginPage.passwordInput, 'secret_sauce') + .click(loginPage.loginButton); + await t.expect(inventoryPage.pageTitle.innerText).eql('Products'); + }); + +test.meta(qase.id(4).title('Browse all products').fields({ + severity: 'critical', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tInventory\tBrowse').create())('Browse products', async t => { + await qase.step('Verify inventory page title', async () => { + await t.expect(inventoryPage.pageTitle.innerText).eql('Products', 'Page title should be Products'); + }); + + await qase.step('Verify product count', async () => { + const itemCount = await inventoryPage.items.count; + await t.expect(itemCount).eql(6, 'Should display 6 products'); + }); + + await qase.step('Verify product details visible', async (s1) => { + await s1.step('Check product names', async () => { + const nameCount = await inventoryPage.itemNames.count; + await t.expect(nameCount).gte(1, 'Product names should be visible'); + }); + + await s1.step('Check product prices', async () => { + const priceCount = await inventoryPage.itemPrices.count; + await t.expect(priceCount).gte(1, 'Product prices should be visible'); + }); + }); + + await qase.comment('All products successfully displayed with correct details'); + + // Gather product data for attachment + const productData = { + totalProducts: await inventoryPage.items.count, + timestamp: new Date().toISOString() + }; + + await qase.attach({ + name: 'product-inventory.json', + content: JSON.stringify(productData, null, 2), + type: 'application/json' + }); +}); + +test.meta(qase.id(5).title('Sort products by price').fields({ + severity: 'medium', + priority: 'medium', + layer: 'e2e' +}).suite('E-commerce\tInventory\tSort').parameters({ + sortOption: 'lohi' +}).create())('Sort products', async t => { + await qase.step('Select price low to high sort', async () => { + await t + .click(inventoryPage.sortDropdown) + .click(inventoryPage.sortDropdown.find('option').withText('Price (low to high)')); + }); + + await qase.step('Verify products sorted', async () => { + const firstPrice = await inventoryPage.itemPrices.nth(0).innerText; + const lastPrice = await inventoryPage.itemPrices.nth(5).innerText; + + // Extract numeric values (remove $ and convert to number) + const firstValue = parseFloat(firstPrice.replace('$', '')); + const lastValue = parseFloat(lastPrice.replace('$', '')); + + await t.expect(firstValue).lte(lastValue, 'First product should be cheaper than or equal to last'); + }); + + await qase.comment('Products correctly sorted by price ascending'); +}); + +test.meta(qase.id(6).title('View product details').fields({ + severity: 'medium', + priority: 'medium', + layer: 'e2e' +}).suite('E-commerce\tInventory\tDetails').create())('Product details', async t => { + await qase.step('Click on first product', async () => { + await t.click(inventoryPage.itemNames.nth(0)); + }); + + await qase.step('Verify product detail page loaded', async () => { + const url = await t.eval(() => window.location.href); + await t.expect(url).contains('inventory-item.html', 'Should navigate to product detail page'); + }); + + await qase.step('Verify back button exists', async () => { + const backButton = await t.eval(() => !!document.querySelector('[data-test="back-to-products"]')); + await t.expect(backButton).ok('Back button should be visible on detail page'); + }); + + await qase.comment('Product detail page successfully loaded with navigation'); +}); diff --git a/examples/single/testcafe/tests/login.test.js b/examples/single/testcafe/tests/login.test.js new file mode 100644 index 00000000..7009a14d --- /dev/null +++ b/examples/single/testcafe/tests/login.test.js @@ -0,0 +1,90 @@ +import { qase } from 'testcafe-reporter-qase/qase'; +import loginPage from './pages/LoginPage.js'; +import inventoryPage from './pages/InventoryPage.js'; + +fixture`Login Scenarios` + .page`https://www.saucedemo.com`; + +test.meta(qase.id(1).title('User can login with valid credentials').fields({ + severity: 'critical', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tAuthentication\tLogin').create())('Valid login', async t => { + await qase.step('Navigate to login page', async () => { + await t.expect(loginPage.loginButton.exists).ok('Login button should be visible'); + }); + + await qase.step('Enter valid credentials', async () => { + await t + .typeText(loginPage.usernameInput, 'standard_user') + .typeText(loginPage.passwordInput, 'secret_sauce'); + }); + + await qase.step('Submit login form', async () => { + await t.click(loginPage.loginButton); + }); + + await qase.step('Verify successful login', async () => { + await t.expect(inventoryPage.pageTitle.innerText).eql('Products', 'Should redirect to inventory page'); + }); + + await qase.comment('User successfully logged in with standard credentials'); + await qase.attach({ + name: 'login-credentials.txt', + content: 'Username: standard_user\nPassword: secret_sauce', + type: 'text/plain' + }); +}); + +test.meta(qase.id(2).title('Invalid password shows error').fields({ + severity: 'high', + priority: 'high', + layer: 'e2e' +}).suite('E-commerce\tAuthentication\tLogin').parameters({ + username: 'standard_user', + password: 'wrong_password' +}).create())('Invalid password login', async t => { + await qase.step('Enter invalid credentials', async () => { + await t + .typeText(loginPage.usernameInput, 'standard_user') + .typeText(loginPage.passwordInput, 'wrong_password'); + }); + + await qase.step('Submit login form', async () => { + await t.click(loginPage.loginButton); + }); + + await qase.step('Verify error message', async () => { + await t.expect(loginPage.errorMessage.exists).ok('Error message should be displayed'); + const errorText = await loginPage.errorMessage.innerText; + await t.expect(errorText).contains('Username and password do not match'); + }); + + await qase.comment('Invalid credentials correctly rejected'); +}); + +test.meta(qase.id(3).title('Locked user cannot login').fields({ + severity: 'high', + priority: 'medium', + layer: 'e2e' +}).suite('E-commerce\tAuthentication\tLogin').parameters({ + username: 'locked_out_user' +}).create())('Locked user login', async t => { + await qase.step('Enter locked user credentials', async () => { + await t + .typeText(loginPage.usernameInput, 'locked_out_user') + .typeText(loginPage.passwordInput, 'secret_sauce'); + }); + + await qase.step('Submit login form', async () => { + await t.click(loginPage.loginButton); + }); + + await qase.step('Verify locked user error', async () => { + await t.expect(loginPage.errorMessage.exists).ok('Error message should be displayed'); + const errorText = await loginPage.errorMessage.innerText; + await t.expect(errorText).contains('Sorry, this user has been locked out'); + }); + + await qase.comment('Locked user correctly denied access'); +}); diff --git a/examples/single/testcafe/tests/pages/CartPage.js b/examples/single/testcafe/tests/pages/CartPage.js new file mode 100644 index 00000000..331af5df --- /dev/null +++ b/examples/single/testcafe/tests/pages/CartPage.js @@ -0,0 +1,18 @@ +import { Selector } from 'testcafe'; + +class CartPage { + constructor() { + this.items = Selector('.cart_item'); + this.itemName = Selector('[data-test="inventory-item-name"]'); + this.itemPrice = Selector('[data-test="inventory-item-price"]'); + this.checkoutButton = Selector('[data-test="checkout"]'); + this.continueShoppingButton = Selector('[data-test="continue-shopping"]'); + this.pageTitle = Selector('.title'); + } + + removeButton(productSlug) { + return Selector('[data-test="remove-' + productSlug + '"]'); + } +} + +export default new CartPage(); diff --git a/examples/single/testcafe/tests/pages/CheckoutPage.js b/examples/single/testcafe/tests/pages/CheckoutPage.js new file mode 100644 index 00000000..ed3aabca --- /dev/null +++ b/examples/single/testcafe/tests/pages/CheckoutPage.js @@ -0,0 +1,18 @@ +import { Selector } from 'testcafe'; + +class CheckoutPage { + constructor() { + this.firstNameInput = Selector('[data-test="firstName"]'); + this.lastNameInput = Selector('[data-test="lastName"]'); + this.postalCodeInput = Selector('[data-test="postalCode"]'); + this.continueButton = Selector('[data-test="continue"]'); + this.cancelButton = Selector('[data-test="cancel"]'); + this.finishButton = Selector('[data-test="finish"]'); + this.completeHeader = Selector('.complete-header'); + this.backToProducts = Selector('[data-test="back-to-products"]'); + this.errorMessage = Selector('[data-test="error"]'); + this.pageTitle = Selector('.title'); + } +} + +export default new CheckoutPage(); diff --git a/examples/single/testcafe/tests/pages/InventoryPage.js b/examples/single/testcafe/tests/pages/InventoryPage.js new file mode 100644 index 00000000..be51d677 --- /dev/null +++ b/examples/single/testcafe/tests/pages/InventoryPage.js @@ -0,0 +1,23 @@ +import { Selector } from 'testcafe'; + +class InventoryPage { + constructor() { + this.items = Selector('.inventory_item'); + this.itemNames = Selector('[data-test="inventory-item-name"]'); + this.itemPrices = Selector('[data-test="inventory-item-price"]'); + this.sortDropdown = Selector('.product_sort_container'); + this.cartBadge = Selector('.shopping_cart_badge'); + this.cartLink = Selector('#shopping_cart_container a'); + this.pageTitle = Selector('.title'); + } + + addToCartButton(productSlug) { + return Selector('[data-test="add-to-cart-' + productSlug + '"]'); + } + + removeButton(productSlug) { + return Selector('[data-test="remove-' + productSlug + '"]'); + } +} + +export default new InventoryPage(); diff --git a/examples/single/testcafe/tests/pages/LoginPage.js b/examples/single/testcafe/tests/pages/LoginPage.js new file mode 100644 index 00000000..eee69196 --- /dev/null +++ b/examples/single/testcafe/tests/pages/LoginPage.js @@ -0,0 +1,12 @@ +import { Selector } from 'testcafe'; + +class LoginPage { + constructor() { + this.usernameInput = Selector('[data-test="username"]'); + this.passwordInput = Selector('[data-test="password"]'); + this.loginButton = Selector('[data-test="login-button"]'); + this.errorMessage = Selector('[data-test="error"]'); + } +} + +export default new LoginPage(); diff --git a/examples/single/vitest/README.md b/examples/single/vitest/README.md index 8d99d0f1..7fdd80fa 100644 --- a/examples/single/vitest/README.md +++ b/examples/single/vitest/README.md @@ -1,25 +1,24 @@ -# Vitest Example +# Vitest API Testing Example with Qase Reporter -This is a sample project demonstrating how to write and execute tests using the Vitest framework with integration to -Qase Test Management. +## Overview -## Prerequisites +This example demonstrates realistic API testing scenarios using Vitest with full Qase TestOps integration. The tests make real HTTP requests to [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free fake REST API for testing and prototyping, demonstrating all Qase reporter features including user CRUD operations, post validation and filtering, error handling, and advanced features like nested steps, suite hierarchy, and parameterized tests. -Ensure that the following tools are installed on your machine: +## Prerequisites -1. [Node.js](https://nodejs.org/) (version 18 or higher is recommended) -2. [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) +- [Node.js](https://nodejs.org/) version 18 or higher (required for native `fetch` API) +- [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) -## Setup Instructions +## Installation -1. Clone this repository by running the following commands: +1. Clone this repository: ```bash git clone https://github.com/qase-tms/qase-javascript.git cd qase-javascript/examples/single/vitest ``` -2. Install the project dependencies: +2. Install dependencies: ```bash npm install @@ -27,140 +26,185 @@ Ensure that the following tools are installed on your machine: 3. Configure your Qase project settings: - Update the API token in `vitest.config.ts` or create a `qase.config.json` file - - Set the correct project name + - Set the correct project code - Enable "Create test cases" option in your Qase project settings -4. To run tests locally without Qase reporting: - - ```bash - QASE_MODE=off npm test - ``` - -5. To run tests and upload the results to Qase Test Management: - - ```bash - QASE_MODE=testops npm test - ``` - -## Example Files - -This project contains several test files demonstrating different Qase features: - -| File | Feature | Description | -|------|---------|-------------| -| `id.test.ts` | Test case linking | Links test to Qase test case by ID using `qase(id, name)` wrapper | -| `title.test.ts` | Custom titles | Sets custom test result titles with `qase.title()` | -| `fields.test.ts` | Custom fields | Sets severity, priority, description, and other metadata with `qase.fields()` | -| `suite.test.ts` | Suite organization | Groups tests into suites and sub-suites with `qase.suite()` | -| `steps.test.ts` | Test steps | Defines execution steps with `await qase.step()` using `withQase()` wrapper | -| `attach.test.ts` | Attachments | Attaches files and content to test results with `qase.attach()` | -| `comment.test.ts` | Comments | Adds comments to test results with `qase.comment()` | -| `params.test.ts` | Parameters | Reports parameterized test data with `qase.parameters()` | -| `api.test.ts` | API testing | Demonstrates API testing with Qase integration | -| `e2e.test.ts` | E2E testing | End-to-end testing example with Qase reporting | -| `for.test.ts` | Loop tests | Demonstrates using loops to generate multiple tests | - -## Expected Behavior - -### Running with QASE_MODE=off (Local Development) - -When running tests with `QASE_MODE=off`, tests execute normally without Qase reporting: - -- Tests run and pass/fail as usual -- No data is sent to Qase TestOps -- No Qase API token required -- Output shows standard Vitest test results -- Vitest UI and watch mode work normally - -This mode is useful for local development and debugging. - -### Running with QASE_MODE=testops (CI/CD and Reporting) - -When running tests with `QASE_MODE=testops`, test results are reported to Qase: - -- Tests execute and results are sent to Qase TestOps -- A new test run is created in your Qase project -- Test results include all metadata (steps, attachments, fields, etc.) -- Console output includes Qase test run link -- Requires valid `QASE_TESTOPS_API_TOKEN` and `QASE_TESTOPS_PROJECT` configuration - -**Steps Example (`steps.test.ts`):** -- Uses `withQase(async ({ qase }) => { ... })` wrapper to access step functionality -- Creates test result with multiple named steps using `await qase.step()` -- Each step shows execution status, duration, and any errors -- Nested steps appear hierarchically in Qase -- Steps with expected results and data are captured - -**Attachments Example (`attach.test.ts`):** -- Uses `withQase(async ({ qase }) => { ... })` wrapper to access attach functionality -- Files attached via `paths` option appear in test results -- Content attached via `content` option is uploaded to Qase -- **Note:** Vitest uses `type:` parameter instead of `contentType:` for in-memory attachments -- Attachments are visible in the test run details -- Supports text, JSON, images, and binary files - -**Multi-Project Support:** -- When configured for multi-project reporting, same test results are sent to multiple Qase projects -- Each project can have different test case IDs for the same test - ## Configuration -Example `qase.config.json`: - -```json -{ - "mode": "testops", - "debug": false, - "testops": { - "api": { - "token": "your_api_token_here" - }, - "project": "YOUR_PROJECT_CODE", - "run": { - "title": "Vitest Automated Test Run", - "complete": true - } - } -} -``` +The Qase reporter can be configured using environment variables or configuration files. -Or configure via `vitest.config.ts`: +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) + +Example `vitest.config.ts`: ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + watch: false, + testTimeout: 10000, // API requests may take longer reporters: [ 'default', - [ - 'vitest-qase-reporter', + ['vitest-qase-reporter', { mode: 'testops', + debug: true, testops: { api: { - token: process.env.QASE_TESTOPS_API_TOKEN, + token: process.env.QASE_TESTOPS_API_TOKEN || "", }, - project: 'YOUR_PROJECT_CODE', run: { complete: true, }, + project: process.env.QASE_TESTOPS_PROJECT || "", + uploadAttachments: true, + showPublicReportLink: true, }, - }, + captureLogs: true, + } ], ], }, }); ``` +## Running Tests + +```bash +# Run tests without Qase reporting (default) +npm test + +# Run tests with Qase reporting +QASE_MODE=testops npm test +``` + +### Run Specific Test File + +```bash +npm test api-crud.test.ts +``` + +## Test Scenarios + +This example includes realistic API testing scenarios demonstrating all Qase reporter features: + +### Test Files + +| File | Purpose | Qase Features | +|------|---------|---------------| +| **api-crud.test.ts** | User CRUD operations (GET all, GET by ID, POST create, DELETE) | `qase.title()`, `qase.fields()`, `qase.step()`, `qase.parameters()`, `qase.attach()`, `qase.comment()` | +| **api-posts.test.ts** | Post validation and filtering (GET all, GET by user, GET with comments) | `qase.title()`, `qase.fields()`, `qase.parameters()`, `qase.step()`, `qase.attach()` | +| **api-errors.test.ts** | Error handling (404 responses, invalid endpoints) | `qase.title()`, `qase.fields()`, `qase.parameters()`, `qase.comment()`, `qase.attach()`, `qase.step()` | +| **api-advanced.test.ts** | Advanced features (nested steps, suite hierarchy, parameterized tests, ignored tests) | `qase.suite()`, `qase.step()` (nested), `qase.parameters()`, `qase.comment()`, `qase.ignore()` | + +## Qase Features Demonstrated + +All 9 Qase reporter features are demonstrated in realistic API testing context: + +| Feature | Methods/Patterns | Where Used | Notes | +|---------|-----------------|------------|-------| +| **Test linking** | Wrap with `withQase()` | All test files | Access to full Qase API | +| **Custom titles** | `await qase.title()` | All tests | Descriptive test result titles | +| **Custom fields** | `await qase.fields()` | All tests | Layer, severity, priority metadata | +| **Suite hierarchy** | `await qase.suite()` | api-advanced.test.ts | Use `\t` separator for nesting | +| **Test steps** | `await qase.step()` | All tests | Named execution steps, supports nesting | +| **Attachments** | `await qase.attach()` | api-crud.test.ts, api-posts.test.ts, api-errors.test.ts | **CRITICAL: use `type` parameter, NOT `contentType`** | +| **Comments** | `await qase.comment()` | api-crud.test.ts, api-errors.test.ts, api-advanced.test.ts | Additional context for test results | +| **Parameters** | `await qase.parameters()` | All test files | Test input data and iterations | +| **Ignore tests** | `qase.ignore()` | api-advanced.test.ts | Mark tests to be excluded (NOT async) | + +## Vitest-Specific Patterns + +**CRITICAL differences from other reporters:** + +1. **Import path:** `import { withQase } from 'vitest-qase-reporter/vitest'` + - NOT from base package like Jest/Mocha + +2. **Wrapper pattern:** `withQase(async ({ qase }) => { ... })` + - Wrap test callback to access Qase API + - Must be async function + - Example: + ```typescript + test("my test", withQase(async ({ qase }) => { + await qase.step("step 1", async () => { ... }); + })); + ``` + +3. **Attachment parameter:** Use `type:` NOT `contentType:` + - **Vitest:** `await qase.attach({ name: 'file.json', content: '...', type: 'application/json' })` + - **Jest/Mocha:** `await qase.attach({ name: 'file.json', content: '...', contentType: 'application/json' })` + - This is a key difference that will cause errors if wrong parameter is used + +4. **Async requirements:** MUST `await` ALL qase methods except `qase.ignore()` + - `await qase.title()`, `await qase.fields()`, `await qase.step()`, etc. + - `qase.ignore()` is the ONLY synchronous method + +5. **Suite hierarchy:** Use `\t` (tab character) as separator + - Example: `await qase.suite('API Tests\tAdvanced\tRelationships')` + +## Project Structure + +``` +vitest/ +├── tests/ +│ ├── api-crud.test.ts # User CRUD operations +│ ├── api-posts.test.ts # Post validation and filtering +│ ├── api-errors.test.ts # Error handling scenarios +│ └── api-advanced.test.ts # Advanced Qase features +├── vitest.config.ts # Vitest configuration +├── qase.config.json # Qase reporter configuration (optional) +└── package.json +``` + +## JSONPlaceholder API + +This example uses [JSONPlaceholder](https://jsonplaceholder.typicode.com/) as the test API: + +- **Free fake REST API** for testing and prototyping +- **No authentication required** - publicly accessible +- **Realistic data structure** - users, posts, comments, albums, photos, todos +- **Fake operations** - POST/PUT/PATCH/DELETE requests are faked (not persisted) +- **Stable and reliable** - maintained for testing purposes + +### Available Resources + +- `/users` - 10 users with profile data +- `/posts` - 100 posts across all users +- `/comments` - 500 comments on posts +- `/albums` - 100 albums +- `/photos` - 5000 photos +- `/todos` - 200 todo items + +## Expected Behavior + +When running with `QASE_MODE=testops`: + +- **12+ realistic API tests** execute against JSONPlaceholder +- **Real HTTP requests** are made (requires internet connection) +- **Test results** are reported to Qase TestOps with: + - Named execution steps showing request/validation flow + - Request/response data attached as JSON + - Test parameters (user IDs, post IDs, etc.) + - Custom fields (layer, severity, priority) + - Comments explaining expected failures + - Suite hierarchy for organized reporting +- **Test run link** is displayed in console output + ## Important Notes -- **withQase Wrapper:** For tests using steps or attachments, wrap your test callback with `withQase(async ({ qase }) => { ... })` to access the `qase` object -- **Import Pattern:** Use `import { withQase } from 'vitest-qase-reporter/vitest';` for step/attach functionality -- **Attachment Parameter:** Use `type:` instead of `contentType:` when attaching content from memory -- **TypeScript Support:** Vitest examples use TypeScript (.ts files) with full type safety +- **Node 18+ required** for native `fetch` API (no external HTTP library needed) +- **Attachment parameter:** Always use `type:` NOT `contentType:` - this is critical for Vitest +- **Async/await required:** All qase methods must be awaited except `qase.ignore()` +- **Internet required:** Tests make real API calls to jsonplaceholder.typicode.com +- **Faked writes:** POST/PUT/DELETE operations appear successful but don't persist data +- **TypeScript:** All test files use TypeScript (.test.ts) with full type safety ## Additional Resources -For more details on how to use this integration with Qase Test Management, visit -the [Qase Vitest documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-vitest). +- [Qase Vitest Documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-vitest) +- [JSONPlaceholder Guide](https://jsonplaceholder.typicode.com/guide/) +- [Vitest Documentation](https://vitest.dev/) diff --git a/examples/single/vitest/test/api-advanced.test.ts b/examples/single/vitest/test/api-advanced.test.ts new file mode 100644 index 00000000..45b8cd26 --- /dev/null +++ b/examples/single/vitest/test/api-advanced.test.ts @@ -0,0 +1,110 @@ +import { describe, test, expect } from 'vitest'; +import { withQase } from 'vitest-qase-reporter/vitest'; + +describe("Advanced Qase Features", () => { + test("Complex nested steps - multi-step user and post retrieval", withQase(async ({ qase }) => { + await qase.title("Demonstrate nested step execution"); + await qase.fields({ layer: 'api', severity: 'normal', priority: 'medium' }); + + let userId: number; + let userName: string; + let userPosts: any[]; + + await qase.step("Step 1: Retrieve user data", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); + const user = await response.json(); + + userId = user.id; + userName = user.name; + + await qase.step("Nested: Validate user data completeness", async () => { + expect(user).toHaveProperty('id'); + expect(user).toHaveProperty('name'); + expect(user).toHaveProperty('email'); + }); + }); + + await qase.step("Step 2: Retrieve posts for user", async () => { + const response = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`); + userPosts = await response.json(); + + await qase.step("Nested: Validate posts belong to user", async () => { + expect(userPosts.length).toBeGreaterThan(0); + userPosts.forEach(post => { + expect(post.userId).toBe(userId); + }); + }); + }); + + await qase.step("Step 3: Verify relationship consistency", async () => { + await qase.parameters({ + userId: userId, + userName: userName, + postCount: userPosts.length + }); + + expect(userPosts.length).toBe(10); // Each user has 10 posts + }); + })); + + test("Suite hierarchy - demonstrate nested suite structure", withQase(async ({ qase }) => { + await qase.title("Demonstrate suite hierarchy with tab separators"); + await qase.suite('API Tests\tAdvanced\tRelationships'); + await qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step("Fetch user and their albums", async () => { + const userResponse = await fetch('https://jsonplaceholder.typicode.com/users/1'); + const user = await userResponse.json(); + + const albumsResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${user.id}/albums`); + const albums = await albumsResponse.json(); + + expect(albums.length).toBeGreaterThan(0); + expect(albums[0].userId).toBe(user.id); + }); + + await qase.step("Verify album structure", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/albums/1'); + const album = await response.json(); + + expect(album).toHaveProperty('userId'); + expect(album).toHaveProperty('id'); + expect(album).toHaveProperty('title'); + }); + })); + + test("Parameterized test pattern - demonstrate multiple test parameters", withQase(async ({ qase }) => { + await qase.title("Verify multiple users with different parameters"); + await qase.fields({ layer: 'api', severity: 'normal' }); + + const userIds = [1, 2, 3]; + + for (const userId of userIds) { + await qase.step(`Test user ${userId}`, async () => { + await qase.parameters({ + userId: userId, + iteration: userIds.indexOf(userId) + 1, + totalIterations: userIds.length + }); + + const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user.id).toBe(userId); + expect(user.name).toBeTruthy(); + }); + } + + await qase.step("Verify all users processed", async () => { + await qase.comment(`Successfully validated ${userIds.length} users with different parameters`); + }); + })); + + test.skip("Authentication endpoint placeholder", () => { + qase.ignore(); + // This test is intentionally skipped to demonstrate qase.ignore() + // Future feature: test OAuth authentication flow + // Note: qase.ignore() is NOT async, unlike other qase methods + }); +}); diff --git a/examples/single/vitest/test/api-crud.test.ts b/examples/single/vitest/test/api-crud.test.ts new file mode 100644 index 00000000..5ff81d5b --- /dev/null +++ b/examples/single/vitest/test/api-crud.test.ts @@ -0,0 +1,115 @@ +import { describe, test, expect } from 'vitest'; +import { withQase } from 'vitest-qase-reporter/vitest'; + +describe("User CRUD Operations", () => { + test("GET all users - verify 10 users returned", withQase(async ({ qase }) => { + await qase.title("Retrieve all users from JSONPlaceholder API"); + await qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step("Send GET request to /users endpoint", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users'); + expect(response.status).toBe(200); + + const users = await response.json(); + expect(users).toHaveLength(10); + }); + + await qase.step("Validate user data structure", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users'); + const users = await response.json(); + + // Verify first user has required fields + expect(users[0]).toHaveProperty('id'); + expect(users[0]).toHaveProperty('name'); + expect(users[0]).toHaveProperty('email'); + expect(users[0]).toHaveProperty('address'); + }); + })); + + test("GET single user by ID - verify user 1 is Leanne Graham", withQase(async ({ qase }) => { + await qase.title("Retrieve specific user by ID"); + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.parameters({ userId: 1 }); + + await qase.step("Send GET request to /users/1", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); + expect(response.status).toBe(200); + + const user = await response.json(); + expect(user.id).toBe(1); + expect(user.name).toBe('Leanne Graham'); + expect(user.email).toBe('Sincere@april.biz'); + }); + + await qase.step("Validate user address structure", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); + const user = await response.json(); + + expect(user.address).toHaveProperty('street'); + expect(user.address).toHaveProperty('city'); + expect(user.address).toHaveProperty('zipcode'); + expect(user.address.geo).toHaveProperty('lat'); + expect(user.address.geo).toHaveProperty('lng'); + }); + })); + + test("POST create user - verify 201 response and returned ID", withQase(async ({ qase }) => { + await qase.title("Create new user via POST request"); + await qase.fields({ layer: 'api', severity: 'critical', priority: 'high' }); + + const newUser = { + name: 'John Doe', + username: 'johndoe', + email: 'john.doe@example.com' + }; + + await qase.step("Send POST request with new user data", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newUser) + }); + + expect(response.status).toBe(201); + + const createdUser = await response.json(); + expect(createdUser).toHaveProperty('id'); + expect(createdUser.id).toBeGreaterThan(0); + }); + + await qase.step("Attach request body for debugging", async () => { + await qase.attach({ + name: 'new-user-payload.json', + content: JSON.stringify(newUser, null, 2), + type: 'application/json' + }); + }); + })); + + test("DELETE user - verify 200 response", withQase(async ({ qase }) => { + await qase.title("Delete user by ID"); + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.parameters({ userId: 1 }); + await qase.comment("Note: JSONPlaceholder fakes DELETE requests - no actual data is removed"); + + await qase.step("Send DELETE request to /users/1", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/1', { + method: 'DELETE' + }); + + expect(response.status).toBe(200); + }); + + await qase.step("Verify delete operation succeeded", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/1', { + method: 'DELETE' + }); + + // JSONPlaceholder returns empty object on successful delete + const result = await response.json(); + expect(result).toEqual({}); + }); + })); +}); diff --git a/examples/single/vitest/test/api-errors.test.ts b/examples/single/vitest/test/api-errors.test.ts new file mode 100644 index 00000000..9dbfc577 --- /dev/null +++ b/examples/single/vitest/test/api-errors.test.ts @@ -0,0 +1,78 @@ +import { describe, test, expect } from 'vitest'; +import { withQase } from 'vitest-qase-reporter/vitest'; + +describe("Error Handling", () => { + test("GET non-existent user (404) - verify error status", withQase(async ({ qase }) => { + await qase.title("Verify API returns 404 for non-existent user"); + await qase.fields({ layer: 'api', severity: 'normal', priority: 'medium' }); + await qase.parameters({ userId: 999 }); + await qase.comment("Expected failure: User ID 999 does not exist in JSONPlaceholder database"); + + await qase.step("Send GET request to /users/999", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/999'); + expect(response.status).toBe(404); + }); + + await qase.step("Verify empty response body", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/users/999'); + const data = await response.json(); + + // JSONPlaceholder returns empty object for non-existent resources + expect(data).toEqual({}); + }); + + await qase.step("Document expected 404 behavior", async () => { + await qase.comment("JSONPlaceholder correctly handles non-existent resources with 404 status"); + }); + })); + + test("GET non-existent post (404) - attach error response", withQase(async ({ qase }) => { + await qase.title("Verify API returns 404 for non-existent post"); + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.parameters({ postId: 9999 }); + + let errorResponse: any; + + await qase.step("Send GET request to non-existent post", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts/9999'); + expect(response.status).toBe(404); + + errorResponse = await response.json(); + }); + + await qase.step("Attach error response for debugging", async () => { + await qase.attach({ + name: '404-error-response.json', + content: JSON.stringify(errorResponse, null, 2), + type: 'application/json' + }); + }); + + await qase.step("Verify error response structure", async () => { + // JSONPlaceholder returns empty object for 404 + expect(errorResponse).toEqual({}); + }); + })); + + test("GET invalid endpoint (404) - verify graceful handling", withQase(async ({ qase }) => { + await qase.title("Verify API handles invalid endpoints gracefully"); + await qase.fields({ layer: 'api', severity: 'normal' }); + + await qase.step("Send request to invalid endpoint", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/invalid-endpoint'); + expect(response.status).toBe(404); + }); + + await qase.step("Verify no server error", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/invalid-endpoint'); + + // Should be 404, not 500 - graceful handling + expect(response.status).not.toBe(500); + expect(response.status).toBe(404); + }); + + await qase.step("Document graceful failure", async () => { + await qase.comment("API correctly returns 404 for invalid endpoints without server errors"); + }); + })); +}); diff --git a/examples/single/vitest/test/api-posts.test.ts b/examples/single/vitest/test/api-posts.test.ts new file mode 100644 index 00000000..95695074 --- /dev/null +++ b/examples/single/vitest/test/api-posts.test.ts @@ -0,0 +1,101 @@ +import { describe, test, expect } from 'vitest'; +import { withQase } from 'vitest-qase-reporter/vitest'; + +describe("Post Validation", () => { + test("GET all posts - verify 100 posts returned", withQase(async ({ qase }) => { + await qase.title("Retrieve all posts from JSONPlaceholder API"); + await qase.fields({ layer: 'api', priority: 'high', severity: 'normal' }); + + await qase.step("Send GET request to /posts endpoint", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts'); + expect(response.status).toBe(200); + + const posts = await response.json(); + expect(posts).toHaveLength(100); + }); + + await qase.step("Validate post structure", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts'); + const posts = await response.json(); + + // Verify posts have required fields + const firstPost = posts[0]; + expect(firstPost).toHaveProperty('userId'); + expect(firstPost).toHaveProperty('id'); + expect(firstPost).toHaveProperty('title'); + expect(firstPost).toHaveProperty('body'); + }); + })); + + test("GET posts by user ID - verify filtered results", withQase(async ({ qase }) => { + await qase.title("Retrieve posts for specific user"); + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.parameters({ userId: 1, expectedPosts: 10 }); + + await qase.step("Send GET request with userId filter", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts?userId=1'); + expect(response.status).toBe(200); + + const posts = await response.json(); + expect(posts).toHaveLength(10); + }); + + await qase.step("Verify all posts belong to user 1", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts?userId=1'); + const posts = await response.json(); + + // All posts should have userId: 1 + posts.forEach(post => { + expect(post.userId).toBe(1); + }); + }); + + await qase.step("Validate post content is not empty", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts?userId=1'); + const posts = await response.json(); + + posts.forEach(post => { + expect(post.title).toBeTruthy(); + expect(post.body).toBeTruthy(); + expect(post.title.length).toBeGreaterThan(0); + expect(post.body.length).toBeGreaterThan(0); + }); + }); + })); + + test("GET post with comments - verify comment structure", withQase(async ({ qase }) => { + await qase.title("Retrieve post comments and validate structure"); + await qase.fields({ layer: 'api', severity: 'normal' }); + await qase.parameters({ postId: 1 }); + + let comments: any[] = []; + + await qase.step("Fetch comments for post 1", async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts/1/comments'); + expect(response.status).toBe(200); + + comments = await response.json(); + expect(comments.length).toBeGreaterThan(0); + }); + + await qase.step("Validate comment structure", async () => { + const firstComment = comments[0]; + expect(firstComment).toHaveProperty('postId'); + expect(firstComment).toHaveProperty('id'); + expect(firstComment).toHaveProperty('name'); + expect(firstComment).toHaveProperty('email'); + expect(firstComment).toHaveProperty('body'); + + // Verify email format + expect(firstComment.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); + }); + + await qase.step("Attach first comment for reference", async () => { + await qase.attach({ + name: 'first-comment.json', + content: JSON.stringify(comments[0], null, 2), + type: 'application/json' + }); + }); + })); +}); diff --git a/examples/single/vitest/test/api.test.ts b/examples/single/vitest/test/api.test.ts deleted file mode 100644 index 66b8ac10..00000000 --- a/examples/single/vitest/test/api.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -describe("Example: API Testing with Qase", () => { - test("API health check test", withQase(async ({ qase }) => { - await qase.title("Verify API health endpoint"); - await qase.fields({ - layer: "api", - priority: "high", - severity: "critical" - }); - await qase.parameters({ Environment: "staging" }); - - await qase.step("Send GET request to health endpoint", async () => { - // Simulate API call - const mockResponse = { status: 200 }; - expect(mockResponse.status).toBe(200); - }); - - await qase.step("Verify response format", async () => { - // Simulate response validation - const mockResponse = { status: "healthy", timestamp: new Date().toISOString() }; - expect(mockResponse.status).toBe("healthy"); - expect(mockResponse.timestamp).toBeDefined(); - }); - })); - - test("API error handling test", withQase(async ({ qase }) => { - await qase.title("Verify API error handling"); - await qase.fields({ - layer: "api", - priority: "medium", - severity: "normal" - }); - - await qase.step("Send request to non-existent endpoint", async () => { - try { - // Simulate API call to non-existent endpoint - const mockResponse = { status: 404 }; - expect(mockResponse.status).toBe(404); - } catch (error) { - // Error handling is expected - expect(error).toBeDefined(); - } - }); - - await qase.step("Verify error response format", async () => { - const mockErrorResponse = { - error: "Not Found", - status: 404, - message: "The requested resource was not found" - }; - expect(mockErrorResponse.error).toBe("Not Found"); - expect(mockErrorResponse.status).toBe(404); - }); - })); - - test("API authentication test", withQase(async ({ qase }) => { - await qase.title("Verify API authentication"); - await qase.fields({ - layer: "api", - priority: "high", - severity: "major" - }); - - const testCredentials = [ - { username: "valid_user", password: "valid_pass", expected: true }, - { username: "invalid_user", password: "wrong_pass", expected: false } - ]; - - for (const cred of testCredentials) { - await qase.step(`Test authentication with ${cred.username}`, async () => { - await qase.parameters({ - Username: cred.username, - Password: cred.password - }); - - // Simulate authentication check - const isValid = cred.username === "valid_user" && cred.password === "valid_pass"; - expect(isValid).toBe(cred.expected); - }); - } - })); - - test("API performance test", withQase(async ({ qase }) => { - await qase.title("Verify API response time"); - await qase.fields({ - layer: "api", - priority: "medium", - severity: "minor" - }); - - await qase.step("Measure response time", async () => { - const startTime = Date.now(); - - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 100)); - - const endTime = Date.now(); - const responseTime = endTime - startTime; - - await qase.parameters({ ResponseTime: `${responseTime}ms` }); - expect(responseTime).toBeLessThan(1000); // Should be less than 1 second - }); - - await qase.step("Verify response time is acceptable", async () => { - const responseTime = 100; // Mock value - expect(responseTime).toBeLessThan(500); // Performance threshold - }); - })); -}); diff --git a/examples/single/vitest/test/attach.test.ts b/examples/single/vitest/test/attach.test.ts deleted file mode 100644 index d7296d0f..00000000 --- a/examples/single/vitest/test/attach.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -describe("Example: attach.test.ts", () => { - test("Test result with attachment", withQase(async ({ qase }) => { - - // To attach a single file - await qase.attach({ - paths: ["./test/attachments/test-file.txt"], - }); - - /* - // Add multiple attachments. - await qase.attach({ paths: ['/path/to/file', '/path/to/another/file'] }); - - */ - // Upload file's contents directly from code. - await qase.attach({ - name: "attachment.txt", - content: "Hello, world!", - type: "text/plain", - }); - - expect(true).toBe(true); - })); -}); diff --git a/examples/single/vitest/test/attachments/test-file.txt b/examples/single/vitest/test/attachments/test-file.txt deleted file mode 100644 index 6815b365..00000000 --- a/examples/single/vitest/test/attachments/test-file.txt +++ /dev/null @@ -1,4 +0,0 @@ -This is a test file for attachments demonstration. -It contains some sample text that will be uploaded to Qase as an attachment. - -You can use this file to test the attachment functionality in your tests. diff --git a/examples/single/vitest/test/comment.test.ts b/examples/single/vitest/test/comment.test.ts deleted file mode 100644 index 2a885c54..00000000 --- a/examples/single/vitest/test/comment.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -describe("Example: comment.test.ts", () => { - test("A test case with qase.comment()", withQase(async ({ qase }) => { - /* - * Please note, this comment is added to a Result, not to the Test case. - */ - - await qase.comment( - "This comment will be displayed in the 'Actual Result' field of the test result in Qase.", - ); - - expect(true).toBe(true); - })); -}); diff --git a/examples/single/vitest/test/e2e.test.ts b/examples/single/vitest/test/e2e.test.ts deleted file mode 100644 index e9805348..00000000 --- a/examples/single/vitest/test/e2e.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -// Mock browser functions for demonstration -const mockBrowser = { - goto: async (url: string) => { - console.log(`Navigating to: ${url}`); - return { status: 200 }; - }, - click: async (selector: string) => { - console.log(`Clicking element: ${selector}`); - return true; - }, - fill: async (selector: string, value: string) => { - console.log(`Filling ${selector} with: ${value}`); - return true; - }, - text: async (selector: string) => { - return "Mock text content"; - }, - screenshot: async () => { - return "screenshot.png"; - } -}; - -describe("Example: E2E Testing with Qase", () => { - test("User login flow", withQase(async ({ qase }) => { - await qase.title("Complete user login flow"); - await qase.fields({ - layer: "e2e", - priority: "high", - severity: "critical" - }); - await qase.parameters({ Browser: "Chrome", Environment: "staging" }); - - await qase.step("Navigate to login page", async () => { - const response = await mockBrowser.goto("https://example.com/login"); - expect(response.status).toBe(200); - }); - - await qase.step("Enter valid credentials", async () => { - await mockBrowser.fill("#username", "testuser"); - await mockBrowser.fill("#password", "password123"); - expect(true).toBe(true); - }); - - await qase.step("Click login button", async () => { - await mockBrowser.click("#login-button"); - expect(true).toBe(true); - }); - - await qase.step("Verify successful login", async () => { - const welcomeText = await mockBrowser.text(".welcome-message"); - expect(welcomeText).toContain("Welcome"); - }); - - await qase.step("Take screenshot of dashboard", async () => { - const screenshot = await mockBrowser.screenshot(); - await qase.attach({ - name: "dashboard-screenshot.png", - content: screenshot, - type: "image/png" - }); - }); - })); - - test("Product search functionality", withQase(async ({ qase }) => { - await qase.title("Product search and filtering"); - await qase.fields({ - layer: "e2e", - priority: "medium", - severity: "normal" - }); - - const searchTerms = ["laptop", "phone", "tablet"]; - - for (const term of searchTerms) { - await qase.step(`Search for ${term}`, async () => { - await qase.parameters({ SearchTerm: term }); - - await mockBrowser.fill("#search-input", term); - await mockBrowser.click("#search-button"); - - const results = await mockBrowser.text(".search-results"); - expect(results).toBeDefined(); - }); - } - - await qase.step("Apply price filter", async () => { - await mockBrowser.click("#price-filter"); - await mockBrowser.fill("#min-price", "100"); - await mockBrowser.fill("#max-price", "1000"); - await mockBrowser.click("#apply-filter"); - - const filteredResults = await mockBrowser.text(".filtered-results"); - expect(filteredResults).toBeDefined(); - }); - })); - - test("Shopping cart functionality", withQase(async ({ qase }) => { - await qase.title("Shopping cart operations"); - await qase.fields({ - layer: "e2e", - priority: "high", - severity: "major" - }); - - await qase.step("Add item to cart", async () => { - await mockBrowser.click(".add-to-cart"); - const cartCount = await mockBrowser.text(".cart-count"); - expect(cartCount).toBe("1"); - }); - - await qase.step("View cart contents", async () => { - await mockBrowser.click(".cart-icon"); - const cartItems = await mockBrowser.text(".cart-items"); - expect(cartItems).toBeDefined(); - }); - - await qase.step("Update item quantity", async () => { - await mockBrowser.fill(".quantity-input", "2"); - await mockBrowser.click(".update-quantity"); - - const updatedCount = await mockBrowser.text(".cart-count"); - expect(updatedCount).toBe("2"); - }); - - await qase.step("Remove item from cart", async () => { - await mockBrowser.click(".remove-item"); - const emptyCart = await mockBrowser.text(".empty-cart"); - expect(emptyCart).toContain("Cart is empty"); - }); - })); - - test("Form validation", withQase(async ({ qase }) => { - await qase.title("Form validation and error handling"); - await qase.fields({ - layer: "e2e", - priority: "medium", - severity: "minor" - }); - - const invalidInputs = [ - { field: "email", value: "invalid-email", expectedError: "Invalid email format" }, - { field: "phone", value: "123", expectedError: "Phone number too short" }, - { field: "password", value: "weak", expectedError: "Password too weak" } - ]; - - for (const input of invalidInputs) { - await qase.step(`Test ${input.field} validation`, async () => { - await qase.parameters({ - Field: input.field, - Value: input.value - }); - - await mockBrowser.fill(`#${input.field}`, input.value); - await mockBrowser.click("#submit-button"); - - const errorMessage = await mockBrowser.text(`.${input.field}-error`); - expect(errorMessage).toContain(input.expectedError); - }); - } - - await qase.step("Test successful form submission", async () => { - await mockBrowser.fill("#email", "valid@email.com"); - await mockBrowser.fill("#phone", "1234567890"); - await mockBrowser.fill("#password", "StrongPassword123!"); - await mockBrowser.click("#submit-button"); - - const successMessage = await mockBrowser.text(".success-message"); - expect(successMessage).toContain("Form submitted successfully"); - }); - })); -}); diff --git a/examples/single/vitest/test/fields.test.ts b/examples/single/vitest/test/fields.test.ts deleted file mode 100644 index 85f64f79..00000000 --- a/examples/single/vitest/test/fields.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; -import { markdownContent } from './markdownContent'; - -describe("Example: fields.test.ts\tTest cases with field: Priority", () => { - /* - * Meta data such as Priority, Severity, Layer fields, Description, and pre-conditions can be updated from code. - * This enables you to manage test cases from code directly. - */ - - test("Priority = low", withQase(async ({ qase }) => { - await qase.fields({ priority: "low" }); - expect(true).toBe(true); - })); - - test("Priority = medium", withQase(async ({ qase }) => { - await qase.fields({ priority: "medium" }); - expect(true).toBe(true); - })); - - test("Priority = high", withQase(async ({ qase }) => { - await qase.fields({ priority: "high" }); - expect(true).toBe(true); - })); -}); - -describe("Example: fields.test.ts\tTest cases with field: Severity", () => { - test("Severity = trivial", withQase(async ({ qase }) => { - await qase.fields({ severity: "trivial" }); - expect(true).toBe(true); - })); - - test("Severity = minor", withQase(async ({ qase }) => { - await qase.fields({ severity: "minor" }); - expect(true).toBe(true); - })); - - test("Severity = normal", withQase(async ({ qase }) => { - await qase.fields({ severity: "normal" }); - expect(true).toBe(true); - })); - - test("Severity = major", withQase(async ({ qase }) => { - await qase.fields({ severity: "major" }); - expect(true).toBe(true); - })); - - test("Severity = critical", withQase(async ({ qase }) => { - await qase.fields({ severity: "critical" }); - expect(true).toBe(true); - })); - - test("Severity = blocker", withQase(async ({ qase }) => { - await qase.fields({ severity: "blocker" }); - expect(true).toBe(true); - })); -}); - -describe("Example: fields.test.ts\tTest cases with field: Layer", () => { - test("Layer = e2e", withQase(async ({ qase }) => { - await qase.fields({ layer: "e2e" }); - expect(true).toBe(true); - })); - - test("Layer = api", withQase(async ({ qase }) => { - await qase.fields({ layer: "api" }); - expect(true).toBe(true); - })); - - test("Layer = unit", withQase(async ({ qase }) => { - await qase.fields({ layer: "unit" }); - expect(true).toBe(true); - })); -}); - -describe("Example: fields.test.ts\tTest cases with Description, Pre & Post Conditions", () => { - test("Description with Markdown Support", withQase(async ({ qase }) => { - await qase.fields({ description: markdownContent }); - expect(true).toBe(true); - })); - - test("Preconditions with Markdown Support", withQase(async ({ qase }) => { - await qase.fields({ preconditions: markdownContent }); - expect(true).toBe(true); - })); - - test("Postconditions with Markdown Support", withQase(async ({ qase }) => { - await qase.fields({ postconditions: markdownContent }); - expect(true).toBe(true); - })); -}); diff --git a/examples/single/vitest/test/for.test.ts b/examples/single/vitest/test/for.test.ts deleted file mode 100644 index 3587dbfd..00000000 --- a/examples/single/vitest/test/for.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { withQase } from "vitest-qase-reporter/vitest"; -import { describe, it, expect } from "vitest"; - -describe("For loop example", () => { - it.for([ - { id: 100, name: "Should be true" }, - { id: 200, name: "Should be false" }, - ])( - "Should be true (Qase ID: $id)", - ((params: { id: number; name: string }, context: { annotate: any }) => { - const testFn = withQase<[{ id: number; name: string }]>(async ({ qase, name }) => { - console.log(name); - - await qase.step(name, () => { - expect(true).toBe(true); - }); - - await qase.step(name, () => { - expect(false).toBe(true); - }); - }); - // Combine params from it.for with Vitest context - return testFn({ ...params, annotate: context.annotate }); - }) as any - ); -}); diff --git a/examples/single/vitest/test/id.test.ts b/examples/single/vitest/test/id.test.ts deleted file mode 100644 index cac0a16c..00000000 --- a/examples/single/vitest/test/id.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { addQaseId } from 'vitest-qase-reporter/vitest'; - -describe("Example: id.test.ts", () => { - // Please, change the Id from `1` to any case Id present in your project before uncommenting the test. - test(addQaseId("A test with Qase Id", [1]), () => { - expect(true).toBe(true); - }); -}); diff --git a/examples/single/vitest/test/markdownContent.ts b/examples/single/vitest/test/markdownContent.ts deleted file mode 100644 index 3f3583c6..00000000 --- a/examples/single/vitest/test/markdownContent.ts +++ /dev/null @@ -1,111 +0,0 @@ -export const markdownContent = `# Markdown Syntax Showcase - -## Headers -### Different Header Levels -#### Are Supported -##### In Markdown -###### Even Smallest Headers - -
- -## Text Formatting -*Italic Text* -**Bold Text** -***Bold and Italic*** -~~Strikethrough Text~~ - -
- -## Lists -### Unordered Lists -- First item -- Second item - * Nested item - * Another nested item - -
- -### Ordered Lists -1. First ordered item -2. Second ordered item - 1. Nested ordered item - 2. Another nested ordered item - -
- -# # Links -[Inline Link](https://www.example.com) -[Link with Title](https://www.example.com "Website Title") - -[Reference-style Link][Reference] -[Reference]: https://www.example.com - -
- -## Code -### Inline Code -Here is some \`inline code\` - -### Code Blocks -\`\`\`javascript -function exampleCode() { - return "Code blocks are supported"; -} -\`\`\` - -\`\`\`python -def python_example(): - return "Multiple language syntax highlighting" -\`\`\` - -
- -## Blockquotes -> This is a blockquote -> -> It can span multiple lines -> -> ### Even with Headers Inside -> -> - And lists -> - Are possible - -
- -## Horizontal Rules ---- -*** -___ - -
- -## Tables -| Column 1 | Column 2 | Column 3 | -|----------|----------|----------| -| Row 1, Col 1 | Row 1, Col 2 | Row 1, Col 3 | -| Row 2, Col 1 | Row 2, Col 2 | Row 2, Col 3 | - -
- -## Task Lists -- [x] Completed task -- [ ] Incomplete task -- [ ] Another incomplete task - -
- -## Footnotes -Here's a sentence with a footnote[^1]. - -[^1]: This is the footnote content. - -
- -## HTML Inline Elements -Some underlined and superscript text. - -
- -## Escaping Characters -\\*This is not italicized\\* -\\# This is a literal hash`; diff --git a/examples/single/vitest/test/params.test.ts b/examples/single/vitest/test/params.test.ts deleted file mode 100644 index a5866dcd..00000000 --- a/examples/single/vitest/test/params.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -const testCases = [ - { browser: "Chromium", username: "@alice", password: "123" }, - { browser: "Firefox", username: "@bob", password: "456" }, - { browser: "Webkit", username: "@charlie", password: "789" }, -]; - -describe("Example param.test.ts\tSingle Parameter", () => { - testCases.forEach(({ browser }) => { - test(`Test login with ${browser}`, withQase(async ({ qase }) => { - await qase.title("Verify if login page loads successfully"); - - /* - * Instead of creating three separate test cases in Qase, this method will add a 'browser' parameter, with three values. - */ - - await qase.parameters({ Browser: browser }); - - expect(true).toBe(true); - })); - }); -}); - -describe("Example param.test.ts\tGroup Parameter", () => { - testCases.forEach(({ username, password }) => { - test(`Test login with ${username} using qase.groupParameters`, withQase(async ({ qase }) => { - await qase.title("Verify if user is able to login with their username."); - - /* - * Here, we're grouping the username and password parameters to track them together, as a set of parameters for the test. - * This will show the username and password combinations for the test. - */ - - await qase.groupParameters({ - Username: username, - Password: password, - }); - - expect(true).toBe(true); - })); - }); -}); diff --git a/examples/single/vitest/test/steps.test.ts b/examples/single/vitest/test/steps.test.ts deleted file mode 100644 index 15c0ec0c..00000000 --- a/examples/single/vitest/test/steps.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -describe("Example: steps.test.ts", () => { - test("A Test case with steps, updated from code", withQase(async ({ qase }) => { - await qase.step("Initialize the environment", async () => { - // Set up test environment - }); - - await qase.step("Test Core Functionality of the app", async () => { - // Exercise core functionality - }); - - await qase.step("Verify Expected Behavior of the app", async () => { - // Assert expected behavior - }); - - await qase.step( - "Verify if user is able to log out successfully", - async () => { - // Expected user to be logged out (but, ran into a problem!). - expect(true).toBe(true); - }, - ); - })); - - test("A Test case with steps including expected results and data", withQase(async ({ qase }) => { - await qase.step("Click button", async () => { - // Click action - }, "Button should be clicked", "Button data"); - - await qase.step("Fill form", async () => { - // Form filling action - }, "Form should be filled", "Form input data"); - - await qase.step("Submit form", async () => { - // Submit action - }, "Form should be submitted", "Form submission data"); - })); -}); diff --git a/examples/single/vitest/test/suite.test.ts b/examples/single/vitest/test/suite.test.ts deleted file mode 100644 index 260f5d59..00000000 --- a/examples/single/vitest/test/suite.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -describe("Example: suite.test.ts", () => { - test("Test with a defined suite", withQase(async ({ qase }) => { - await qase.suite("Example: suite.test.ts\tThis shall be a suite name"); - expect(true).toBe(true); - })); - - test("Test within multiple levels of suite", withQase(async ({ qase }) => { - await qase.suite( - "Example: suite.test.ts\tThis shall be a suite name\tChild Suite", - ); - // A `\t` is used for dividing each suite name - expect(true).toBe(true); - })); -}); diff --git a/examples/single/vitest/test/title.test.ts b/examples/single/vitest/test/title.test.ts deleted file mode 100644 index 7990c5d3..00000000 --- a/examples/single/vitest/test/title.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { withQase } from 'vitest-qase-reporter/vitest'; - -describe("Example: title.test.ts", () => { - test("Test without qase.title() method", () => { - /* - * Here, we're are not using a qase.title() method - * Given, you have "Auto-create cases" option enabled for this project. - * A new test will be created in Qase, with the test's title. - */ - - expect(true).toBe(true); - }); - - test("This won't appear in Qase", withQase(async ({ qase }) => { - await qase.title("This text will be the title of the test, in Qase"); - - /* - * Here, the Qase Test case's title will be taken from qase.title() method. - */ - - expect(true).toBe(true); - })); -}); - -/* - * - * Q) What about the tests where the qase.title() method is not used? - * => Those test cases will have the "Title of this test" as the newly created case's title. - * - * - * Q) I'm running this test case, but it's not creating any test case in Qase. - * My test run is empty, what am I doing wrong? - * - * => Go to your Qase Project's settings, switch to the Test runs tab. - * Under "Automated Testing" - Enable "Create test cases option" [https://i.imgur.com/PtZPrrY.png] - * - * - * Q) What happens if I change the title in `qase.title()` ? - * => Since, there's no link between the Qase test case and this test, changing the title will lead to - * a new case being created in your Project repository. - * - */ diff --git a/examples/single/vitest/vitest.config.ts b/examples/single/vitest/vitest.config.ts index 82bcf75f..7afc62e6 100644 --- a/examples/single/vitest/vitest.config.ts +++ b/examples/single/vitest/vitest.config.ts @@ -4,6 +4,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { watch: false, + testTimeout: 10000, reporters: [ 'default', ['vitest-qase-reporter', diff --git a/examples/single/wdio/.gitignore b/examples/single/wdio/.gitignore new file mode 100644 index 00000000..51dd7468 --- /dev/null +++ b/examples/single/wdio/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +logs/ diff --git a/examples/single/wdio/README.md b/examples/single/wdio/README.md new file mode 100644 index 00000000..a6ba5afa --- /dev/null +++ b/examples/single/wdio/README.md @@ -0,0 +1,220 @@ +# WDIO Example - E-commerce Test Suite + +## Overview + +This example demonstrates realistic e-commerce test scenarios on [saucedemo.com](https://www.saucedemo.com) using WebdriverIO with Qase TestOps integration. The test suite covers core e-commerce user flows: user authentication (login scenarios), product catalog browsing and sorting, shopping cart management, and complete checkout process. All tests demonstrate comprehensive Qase reporter features in realistic test contexts using the Page Object Model pattern. + +**Note:** This is a **single project** example. For multi-project usage patterns, see `examples/multiProject/wdio/`. + +## Prerequisites + +- Node.js 18 or higher +- npm +- Chrome browser (tests run in headless mode) + +## Installation + +```bash +# Install dependencies +npm install +``` + +## Configuration + +The Qase reporter can be configured using environment variables or configuration files. + +**Environment Variables:** +- `QASE_MODE` - Set to `testops` to enable reporting, `off` to disable (default: off) +- `QASE_TESTOPS_API_TOKEN` - Your Qase API token (required for testops mode) +- `QASE_TESTOPS_PROJECT` - Your Qase project code (required for testops mode) + +### qase.config.json + +Single project configuration: + +```json +{ + "debug": true, + "testops": { + "api": { + "token": "your_api_token" + }, + "project": "your_project_code", + "uploadAttachments": true, + "run": { + "complete": true + } + } +} +``` + +### wdio.conf.js + +WebdriverIO configuration with Qase reporter: + +```javascript +const WDIOQaseReporter = require('wdio-qase-reporter').default; +const { afterRunHook, beforeRunHook } = require('wdio-qase-reporter'); + +exports.config = { + reporters: [ + [WDIOQaseReporter, { + disableWebdriverStepsReporting: true, + disableWebdriverScreenshotsReporting: true, + useCucumber: false, + }] + ], + // ... critical hooks + onPrepare: async function () { + await beforeRunHook(); + }, + onComplete: async function () { + await afterRunHook(); + }, +}; +``` + +## Running Tests + +```bash +# Run tests without Qase reporting (default) +npm test + +# Run tests with Qase reporting +QASE_MODE=testops npm test +``` + +## Test Scenarios + +| File | Scenarios | Features Demonstrated | +|------|-----------|----------------------| +| `login.spec.js` | Valid login, invalid credentials, locked user | qase.id, fields, suite, steps, comment, attach, parameters | +| `inventory.spec.js` | Browse products, sort by price, view details | Multiple IDs `qase([6, 7], 'name')`, JSON attachments | +| `cart.spec.js` | Add to cart, remove from cart, multiple items | qase.step, parameters, JSON attachments | +| `checkout.spec.js` | Complete checkout, validation errors, cancel | Nested steps `step.step()`, qase.ignore() | + +## Qase Features Demonstrated + +| Feature | Usage Pattern | Example Location | +|---------|--------------|------------------| +| Test ID | `it(qase(1, 'name'), async () => {})` | All test files | +| Multiple IDs | `it(qase([6, 7], 'name'), async () => {})` | `inventory.spec.js` | +| Title | Provided as second argument in wrapper | All tests | +| Fields | `qase.fields({severity, priority, layer})` | All tests | +| Suite | `qase.suite('Parent\tChild\tGrandchild')` | All tests | +| Steps | `await qase.step('name', async () => {})` | All tests | +| Nested Steps | `await qase.step('parent', async (step) => { await step.step('child', async () => {}) })` | `checkout.spec.js` | +| Parameters | `qase.parameters({key: 'value'})` | Various tests | +| Attachments | `qase.attach({name, content, type})` | `login.spec.js`, `inventory.spec.js`, `cart.spec.js`, `checkout.spec.js` | +| Comment | `qase.comment('text')` | All tests | +| Ignore | `it(qase.ignore(), 'name', async () => {})` | `checkout.spec.js` | + +## WDIO-Specific Patterns + +### CommonJS (not ES modules) +```javascript +const { qase } = require('wdio-qase-reporter'); +const LoginPage = require('../pageobjects/LoginPage'); +``` + +### Wrapper Pattern for Test IDs +```javascript +// Correct: wrap test name with qase() +it(qase(1, 'Test name'), async () => { + // test code +}); + +// Multiple IDs +it(qase([1, 2], 'Test name'), async () => { + // test code +}); +``` + +### Async Steps +```javascript +await qase.step('Step name', async () => { + // step code +}); +``` + +### Attachment Type Parameter +```javascript +// Use 'type' parameter (NOT 'contentType') +qase.attach({ + name: 'file.json', + content: JSON.stringify(data), + type: 'application/json' +}); +``` + +### Critical Hooks in wdio.conf.js +```javascript +const { beforeRunHook, afterRunHook } = require('wdio-qase-reporter'); + +exports.config = { + // ... other config + onPrepare: async function () { + await beforeRunHook(); // Required for Qase integration + }, + onComplete: async function () { + await afterRunHook(); // Required for Qase integration + }, +}; +``` + +## Project Structure + +``` +wdio/ +├── test/ +│ ├── pageobjects/ +│ │ ├── LoginPage.js # Login page interactions +│ │ ├── InventoryPage.js # Product catalog and sorting +│ │ ├── CartPage.js # Shopping cart management +│ │ └── CheckoutPage.js # Checkout process +│ ├── specs/ +│ │ ├── login.spec.js # Authentication test scenarios +│ │ ├── inventory.spec.js # Product browsing test scenarios +│ │ ├── cart.spec.js # Shopping cart test scenarios +│ │ └── checkout.spec.js # Checkout test scenarios +├── wdio.conf.js # WebdriverIO configuration +├── qase.config.json # Qase reporter configuration +└── package.json +``` + +## Page Objects + +This example uses the **WDIO Page Object Pattern** with getter methods: + +```javascript +class LoginPage { + get usernameInput() { return $('[data-test="username"]'); } + get passwordInput() { return $('[data-test="password"]'); } + + async login(username, password) { + await this.usernameInput.setValue(username); + await this.passwordInput.setValue(password); + await this.loginButton.click(); + } +} + +module.exports = new LoginPage(); +``` + +Page objects are located in `test/pageobjects/`: +- `LoginPage.js` - Login page interactions +- `InventoryPage.js` - Product catalog and sorting +- `CartPage.js` - Shopping cart management +- `CheckoutPage.js` - Checkout process + +## Credentials + +For testing on saucedemo.com, use these credentials: +- **Standard User:** `standard_user` / `secret_sauce` +- **Locked User:** `locked_out_user` / `secret_sauce` (for negative testing) + +## Additional Resources + +- [Qase WDIO Reporter Documentation](https://github.com/qase-tms/qase-javascript/tree/main/qase-wdio) +- [WebdriverIO Documentation](https://webdriver.io/) +- [Saucedemo Test Site](https://www.saucedemo.com) diff --git a/examples/single/wdio/package.json b/examples/single/wdio/package.json new file mode 100644 index 00000000..55bacc22 --- /dev/null +++ b/examples/single/wdio/package.json @@ -0,0 +1,14 @@ +{ + "name": "examples-wdio", + "private": true, + "scripts": { + "test": "QASE_MODE=${QASE_MODE:-off} wdio run ./wdio.conf.js" + }, + "devDependencies": { + "@wdio/cli": "^8.40.0", + "@wdio/local-runner": "^8.40.0", + "@wdio/mocha-framework": "^8.40.0", + "wdio-chromedriver-service": "^8.0.0", + "wdio-qase-reporter": "^1.1.4" + } +} diff --git a/examples/single/wdio/qase.config.json b/examples/single/wdio/qase.config.json new file mode 100644 index 00000000..b255dc2d --- /dev/null +++ b/examples/single/wdio/qase.config.json @@ -0,0 +1,13 @@ +{ + "debug": true, + "testops": { + "api": { + "token": "api_key" + }, + "project": "project_code", + "uploadAttachments": true, + "run": { + "complete": true + } + } +} diff --git a/examples/single/wdio/test/pageobjects/CartPage.js b/examples/single/wdio/test/pageobjects/CartPage.js new file mode 100644 index 00000000..24052260 --- /dev/null +++ b/examples/single/wdio/test/pageobjects/CartPage.js @@ -0,0 +1,22 @@ +class CartPage { + get items() { return $$('.cart_item'); } + get itemName() { return $('[data-test="inventory-item-name"]'); } + get itemPrice() { return $('[data-test="inventory-item-price"]'); } + get checkoutButton() { return $('[data-test="checkout"]'); } + get continueShoppingButton() { return $('[data-test="continue-shopping"]'); } + get pageTitle() { return $('.title'); } + + async removeItem(productSlug) { + await $('[data-test="remove-' + productSlug + '"]').click(); + } + + async checkout() { + await this.checkoutButton.click(); + } + + async continueShopping() { + await this.continueShoppingButton.click(); + } +} + +module.exports = new CartPage(); diff --git a/examples/single/wdio/test/pageobjects/CheckoutPage.js b/examples/single/wdio/test/pageobjects/CheckoutPage.js new file mode 100644 index 00000000..31452ecc --- /dev/null +++ b/examples/single/wdio/test/pageobjects/CheckoutPage.js @@ -0,0 +1,32 @@ +class CheckoutPage { + get firstNameInput() { return $('[data-test="firstName"]'); } + get lastNameInput() { return $('[data-test="lastName"]'); } + get postalCodeInput() { return $('[data-test="postalCode"]'); } + get continueButton() { return $('[data-test="continue"]'); } + get cancelButton() { return $('[data-test="cancel"]'); } + get finishButton() { return $('[data-test="finish"]'); } + get completeHeader() { return $('.complete-header'); } + get backToProducts() { return $('[data-test="back-to-products"]'); } + get errorMessage() { return $('[data-test="error"]'); } + get pageTitle() { return $('.title'); } + + async fillInfo(firstName, lastName, postalCode) { + await this.firstNameInput.setValue(firstName); + await this.lastNameInput.setValue(lastName); + await this.postalCodeInput.setValue(postalCode); + } + + async continue() { + await this.continueButton.click(); + } + + async finish() { + await this.finishButton.click(); + } + + async cancel() { + await this.cancelButton.click(); + } +} + +module.exports = new CheckoutPage(); diff --git a/examples/single/wdio/test/pageobjects/InventoryPage.js b/examples/single/wdio/test/pageobjects/InventoryPage.js new file mode 100644 index 00000000..7cde54b9 --- /dev/null +++ b/examples/single/wdio/test/pageobjects/InventoryPage.js @@ -0,0 +1,27 @@ +class InventoryPage { + get items() { return $$('.inventory_item'); } + get itemNames() { return $$('[data-test="inventory-item-name"]'); } + get itemPrices() { return $$('[data-test="inventory-item-price"]'); } + get sortDropdown() { return $('.product_sort_container'); } + get cartBadge() { return $('.shopping_cart_badge'); } + get cartLink() { return $('#shopping_cart_container a'); } + get pageTitle() { return $('.title'); } + + async addToCart(productSlug) { + await $('[data-test="add-to-cart-' + productSlug + '"]').click(); + } + + async removeFromCart(productSlug) { + await $('[data-test="remove-' + productSlug + '"]').click(); + } + + async goToCart() { + await this.cartLink.click(); + } + + async sortBy(value) { + await this.sortDropdown.selectByAttribute('value', value); + } +} + +module.exports = new InventoryPage(); diff --git a/examples/single/wdio/test/pageobjects/LoginPage.js b/examples/single/wdio/test/pageobjects/LoginPage.js new file mode 100644 index 00000000..2809f26a --- /dev/null +++ b/examples/single/wdio/test/pageobjects/LoginPage.js @@ -0,0 +1,18 @@ +class LoginPage { + get usernameInput() { return $('[data-test="username"]'); } + get passwordInput() { return $('[data-test="password"]'); } + get loginButton() { return $('[data-test="login-button"]'); } + get errorMessage() { return $('[data-test="error"]'); } + + async open() { + await browser.url('https://www.saucedemo.com'); + } + + async login(username, password) { + await this.usernameInput.setValue(username); + await this.passwordInput.setValue(password); + await this.loginButton.click(); + } +} + +module.exports = new LoginPage(); diff --git a/examples/single/wdio/test/specs/cart.spec.js b/examples/single/wdio/test/specs/cart.spec.js new file mode 100644 index 00000000..bffe8d3c --- /dev/null +++ b/examples/single/wdio/test/specs/cart.spec.js @@ -0,0 +1,109 @@ +const { qase } = require('wdio-qase-reporter'); +const LoginPage = require('../pageobjects/LoginPage'); +const InventoryPage = require('../pageobjects/InventoryPage'); +const CartPage = require('../pageobjects/CartPage'); + +describe('Cart Management', () => { + beforeEach(async () => { + await LoginPage.open(); + await LoginPage.login('standard_user', 'secret_sauce'); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/inventory.html'), + { timeout: 5000 } + ); + }); + + it(qase(8, 'User can add product to cart'), async () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCart\tAdd Items'); + qase.parameters({ product: 'Sauce Labs Backpack' }); + + await qase.step('Add product to cart', async () => { + await InventoryPage.addToCart('sauce-labs-backpack'); + }); + + await qase.step('Verify cart badge shows item count', async () => { + await expect(InventoryPage.cartBadge).toHaveText('1'); + }); + + await qase.step('Navigate to cart page', async () => { + await InventoryPage.goToCart(); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/cart.html'), + { timeout: 5000 } + ); + }); + + await qase.step('Verify product is in cart', async () => { + await expect(CartPage.pageTitle).toHaveText('Your Cart'); + await expect(CartPage.itemName).toHaveTextContaining('Backpack'); + }); + + const cartState = { + itemCount: 1, + productAdded: 'Sauce Labs Backpack' + }; + + qase.attach({ + name: 'cart-state.json', + content: JSON.stringify(cartState, null, 2), + type: 'application/json' + }); + + qase.comment('Product successfully added to cart and visible on cart page'); + }); + + it(qase(9, 'User can remove product from cart'), async () => { + qase.fields({ severity: 'high', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCart\tRemove Items'); + + await qase.step('Add product to cart', async () => { + await InventoryPage.addToCart('sauce-labs-backpack'); + await expect(InventoryPage.cartBadge).toHaveText('1'); + }); + + await qase.step('Navigate to cart', async () => { + await InventoryPage.goToCart(); + }); + + await qase.step('Remove product from cart', async () => { + await CartPage.removeItem('sauce-labs-backpack'); + }); + + await qase.step('Verify cart is empty', async () => { + const items = await CartPage.items; + expect(items).toHaveLength(0); + }); + + qase.comment('Product successfully removed from cart'); + }); + + it(qase(10, 'User can add multiple products to cart'), async () => { + qase.fields({ severity: 'medium', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tCart\tMultiple Items'); + + await qase.step('Add first product', async () => { + await InventoryPage.addToCart('sauce-labs-backpack'); + }); + + await qase.step('Add second product', async () => { + await InventoryPage.addToCart('sauce-labs-bike-light'); + }); + + await qase.step('Add third product', async () => { + await InventoryPage.addToCart('sauce-labs-bolt-t-shirt'); + }); + + await qase.step('Verify cart badge shows correct count', async () => { + await expect(InventoryPage.cartBadge).toHaveText('3'); + }); + + await qase.step('Navigate to cart and verify all items', async () => { + await InventoryPage.goToCart(); + const items = await CartPage.items; + expect(items).toHaveLength(3); + }); + + qase.comment('Multiple products can be added and are tracked correctly'); + }); +}); diff --git a/examples/single/wdio/test/specs/checkout.spec.js b/examples/single/wdio/test/specs/checkout.spec.js new file mode 100644 index 00000000..ba7b5cd0 --- /dev/null +++ b/examples/single/wdio/test/specs/checkout.spec.js @@ -0,0 +1,118 @@ +const { qase } = require('wdio-qase-reporter'); +const LoginPage = require('../pageobjects/LoginPage'); +const InventoryPage = require('../pageobjects/InventoryPage'); +const CartPage = require('../pageobjects/CartPage'); +const CheckoutPage = require('../pageobjects/CheckoutPage'); + +describe('Checkout Flow', () => { + beforeEach(async () => { + await LoginPage.open(); + await LoginPage.login('standard_user', 'secret_sauce'); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/inventory.html'), + { timeout: 5000 } + ); + await InventoryPage.addToCart('sauce-labs-backpack'); + await InventoryPage.goToCart(); + await CartPage.checkout(); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/checkout-step-one.html'), + { timeout: 5000 } + ); + }); + + it(qase(11, 'User can complete checkout with valid information'), async () => { + qase.fields({ severity: 'critical', priority: 'critical', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tComplete Purchase'); + qase.parameters({ firstName: 'John', lastName: 'Doe', postalCode: '12345' }); + + await qase.step('Fill checkout information', async (step) => { + await step.step('Enter first name', async () => { + await CheckoutPage.firstNameInput.setValue('John'); + }); + + await step.step('Enter last name', async () => { + await CheckoutPage.lastNameInput.setValue('Doe'); + }); + + await step.step('Enter postal code', async () => { + await CheckoutPage.postalCodeInput.setValue('12345'); + }); + }); + + await qase.step('Continue to checkout overview', async () => { + await CheckoutPage.continue(); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/checkout-step-two.html'), + { timeout: 5000 } + ); + }); + + await qase.step('Complete the order', async () => { + await CheckoutPage.finish(); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/checkout-complete.html'), + { timeout: 5000 } + ); + }); + + await qase.step('Verify order completion', async () => { + await expect(CheckoutPage.completeHeader).toHaveTextContaining('Thank you'); + }); + + qase.attach({ + name: 'order-complete.txt', + content: 'Order completed successfully for John Doe at 12345', + type: 'text/plain' + }); + + qase.comment('Checkout completed successfully with nested step demonstration'); + }); + + it(qase(12, 'Checkout fails without required information'), async () => { + qase.fields({ severity: 'high', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tValidation'); + qase.parameters({ scenario: 'missing_first_name' }); + + await qase.step('Leave first name empty and continue', async () => { + await CheckoutPage.lastNameInput.setValue('Doe'); + await CheckoutPage.postalCodeInput.setValue('12345'); + await CheckoutPage.continue(); + }); + + await qase.step('Verify error message is shown', async () => { + await expect(CheckoutPage.errorMessage).toBeDisplayed(); + await expect(CheckoutPage.errorMessage).toHaveTextContaining('First Name is required'); + }); + + qase.comment('Form validation correctly prevents checkout without required fields'); + }); + + it(qase(13, 'User can cancel checkout'), async () => { + qase.fields({ severity: 'low', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tCheckout\tCancel'); + + await qase.step('Fill partial information', async () => { + await CheckoutPage.fillInfo('John', 'Doe', '12345'); + }); + + await qase.step('Click cancel button', async () => { + await CheckoutPage.cancel(); + }); + + await qase.step('Verify return to cart page', async () => { + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/cart.html'), + { timeout: 5000 } + ); + await expect(browser).toHaveUrl('https://www.saucedemo.com/cart.html'); + }); + + qase.comment('User can safely cancel checkout and return to cart'); + }); + + it(qase.ignore(), 'Ignored test example', async () => { + // This test is ignored for demonstration purposes + await expect(true).toBe(true); + }); +}); diff --git a/examples/single/wdio/test/specs/inventory.spec.js b/examples/single/wdio/test/specs/inventory.spec.js new file mode 100644 index 00000000..249dd7c5 --- /dev/null +++ b/examples/single/wdio/test/specs/inventory.spec.js @@ -0,0 +1,97 @@ +const { qase } = require('wdio-qase-reporter'); +const LoginPage = require('../pageobjects/LoginPage'); +const InventoryPage = require('../pageobjects/InventoryPage'); + +describe('Product Inventory', () => { + beforeEach(async () => { + await LoginPage.open(); + await LoginPage.login('standard_user', 'secret_sauce'); + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/inventory.html'), + { timeout: 5000 } + ); + }); + + it(qase(4, 'User can browse all products'), async () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tProduct Catalog\tBrowsing'); + + await qase.step('Verify page title', async () => { + await expect(InventoryPage.pageTitle).toHaveText('Products'); + }); + + await qase.step('Count products displayed', async () => { + const items = await InventoryPage.items; + expect(items).toHaveLength(6); + }); + + await qase.step('Verify product details are visible', async () => { + const names = await InventoryPage.itemNames; + const prices = await InventoryPage.itemPrices; + + expect(await names[0].isDisplayed()).toBe(true); + expect(await prices[0].isDisplayed()).toBe(true); + expect(await prices[0].getText()).toMatch(/\$\d+\.\d{2}/); + }); + + qase.comment('All products are correctly displayed with prices and details'); + + const productData = { + totalProducts: 6, + firstProduct: await (await InventoryPage.itemNames)[0].getText(), + firstPrice: await (await InventoryPage.itemPrices)[0].getText() + }; + + qase.attach({ + name: 'product-list.json', + content: JSON.stringify(productData, null, 2), + type: 'application/json' + }); + }); + + it(qase(5, 'User can sort products by price'), async () => { + qase.fields({ severity: 'medium', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tProduct Catalog\tSorting'); + qase.parameters({ sortOption: 'lohi' }); + + await qase.step('Select sort by price low to high', async () => { + await InventoryPage.sortBy('lohi'); + }); + + await qase.step('Verify products are sorted by ascending price', async () => { + const prices = await InventoryPage.itemPrices; + const priceTexts = await Promise.all(prices.map(async p => await p.getText())); + const priceValues = priceTexts.map(p => parseFloat(p.replace('$', ''))); + + for (let i = 0; i < priceValues.length - 1; i++) { + expect(priceValues[i]).toBeLessThanOrEqual(priceValues[i + 1]); + } + }); + + qase.comment('Products correctly sorted by price in ascending order'); + }); + + it(qase([6, 7], 'User can view product details'), async () => { + qase.fields({ severity: 'medium', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tProduct Catalog\tDetails'); + + let productName; + + await qase.step('Click on product name', async () => { + const names = await InventoryPage.itemNames; + productName = await names[0].getText(); + await names[0].click(); + }); + + await qase.step('Verify navigation to product detail page', async () => { + await browser.waitUntil( + async () => (await browser.getUrl()).includes('inventory-item.html'), + { timeout: 5000 } + ); + expect(await browser.getUrl()).toContain('inventory-item.html'); + }); + + qase.comment('Demonstrates multiple test IDs linked to same test case'); + qase.parameters({ productClicked: productName }); + }); +}); diff --git a/examples/single/wdio/test/specs/login.spec.js b/examples/single/wdio/test/specs/login.spec.js new file mode 100644 index 00000000..8320f886 --- /dev/null +++ b/examples/single/wdio/test/specs/login.spec.js @@ -0,0 +1,75 @@ +const { qase } = require('wdio-qase-reporter'); +const LoginPage = require('../pageobjects/LoginPage'); + +describe('Login Scenarios', () => { + it(qase(1, 'User can login with valid credentials'), async () => { + qase.fields({ severity: 'critical', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + + await qase.step('Open login page', async () => { + await LoginPage.open(); + await expect(LoginPage.usernameInput).toBeDisplayed(); + }); + + await qase.step('Enter valid credentials and submit', async () => { + await LoginPage.login('standard_user', 'secret_sauce'); + }); + + await qase.step('Verify successful login', async () => { + await browser.waitUntil( + async () => (await browser.getUrl()).includes('/inventory.html'), + { timeout: 5000, timeoutMsg: 'Expected to navigate to inventory page' } + ); + await expect(browser).toHaveUrl('https://www.saucedemo.com/inventory.html'); + }); + + qase.comment('Login successful with standard user credentials'); + qase.attach({ + name: 'login-credentials.txt', + content: 'Username: standard_user\nPassword: secret_sauce', + type: 'text/plain' + }); + }); + + it(qase(2, 'User cannot login with invalid password'), async () => { + qase.fields({ severity: 'high', priority: 'high', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'standard_user', password: 'wrong_password' }); + + await qase.step('Open login page', async () => { + await LoginPage.open(); + }); + + await qase.step('Enter invalid credentials', async () => { + await LoginPage.login('standard_user', 'wrong_password'); + }); + + await qase.step('Verify error message is displayed', async () => { + await expect(LoginPage.errorMessage).toBeDisplayed(); + await expect(LoginPage.errorMessage).toHaveTextContaining('Username and password do not match'); + }); + + qase.comment('Error message correctly displayed for invalid credentials'); + }); + + it(qase(3, 'Locked user cannot login'), async () => { + qase.fields({ severity: 'high', priority: 'medium', layer: 'e2e' }); + qase.suite('E-commerce\tAuthentication\tLogin'); + qase.parameters({ username: 'locked_out_user' }); + + await qase.step('Open login page', async () => { + await LoginPage.open(); + }); + + await qase.step('Attempt login with locked user', async () => { + await LoginPage.login('locked_out_user', 'secret_sauce'); + }); + + await qase.step('Verify locked user message', async () => { + await expect(LoginPage.errorMessage).toBeDisplayed(); + await expect(LoginPage.errorMessage).toHaveTextContaining('locked out'); + }); + + qase.comment('System correctly prevents locked users from accessing the application'); + }); +}); diff --git a/examples/single/wdio/wdio.conf.js b/examples/single/wdio/wdio.conf.js new file mode 100644 index 00000000..fde8c2e8 --- /dev/null +++ b/examples/single/wdio/wdio.conf.js @@ -0,0 +1,41 @@ +const WDIOQaseReporter = require('wdio-qase-reporter').default; +const { afterRunHook, beforeRunHook } = require('wdio-qase-reporter'); + +exports.config = { + runner: 'local', + specs: ['./test/specs/**/*.spec.js'], + capabilities: [ + { + maxInstances: 1, + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['--headless', '--disable-gpu'], + }, + }, + ], + logLevel: 'warn', + baseUrl: 'https://www.saucedemo.com', + waitforTimeout: 10000, + connectionRetryTimeout: 120000, + reporters: [ + [ + WDIOQaseReporter, + { + disableWebdriverStepsReporting: true, + disableWebdriverScreenshotsReporting: true, + useCucumber: false, + }, + ], + ], + framework: 'mocha', + mochaOpts: { + ui: 'bdd', + timeout: 60000, + }, + onPrepare: async function () { + await beforeRunHook(); + }, + onComplete: async function () { + await afterRunHook(); + }, +}; diff --git a/qase-testcafe/changelog.md b/qase-testcafe/changelog.md index 116594c8..4da21741 100644 --- a/qase-testcafe/changelog.md +++ b/qase-testcafe/changelog.md @@ -1,3 +1,14 @@ +# qase-testcafe@2.2.1 + +## What's new + +- Added `qase.suite()` method to the builder API. You can now set a custom suite hierarchy for test cases using tab-separated values for nested suites. + +```ts +const q = qase.suite('Parent\tChild').create(); +test.meta(q)('Test case title', async t => { ... }); +``` + # qase-testcafe@2.2.0 ## What's new diff --git a/qase-testcafe/package.json b/qase-testcafe/package.json index 7342cd76..477305b3 100644 --- a/qase-testcafe/package.json +++ b/qase-testcafe/package.json @@ -1,6 +1,6 @@ { "name": "testcafe-reporter-qase", - "version": "2.2.0", + "version": "2.2.1", "description": "Qase TMS TestCafe Reporter", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/qase-testcafe/src/qase.ts b/qase-testcafe/src/qase.ts index 7af491ca..14e957ee 100644 --- a/qase-testcafe/src/qase.ts +++ b/qase-testcafe/src/qase.ts @@ -12,6 +12,7 @@ export class qase { private static _qaseFields = ''; private static _qaseParameters = ''; private static _qaseGroupParameters = ''; + private static _qaseSuite = ''; private static _qaseIgnore = ''; private static _qaseProjects = ''; @@ -60,6 +61,19 @@ export class qase { return this; }; + /** + * Set a suite for the test case + * Don't forget to call `create` method after setting all the necessary parameters + * @param {string} value + * @example + * const q = qase.suite('Suite\tSub-suite').create(); + * test.meta(q)('Test case title', async t => { ... }); + */ + public static suite = (value: string) => { + this._qaseSuite = value; + return this; + }; + /** * Set a fields for the test case * Don't forget to call `create` method after setting all the necessary parameters @@ -205,6 +219,7 @@ export class qase { const meta = { QaseID: this._qaseID, QaseTitle: this._qaseTitle, + QaseSuite: this._qaseSuite, QaseFields: this._qaseFields, QaseParameters: this._qaseParameters, QaseGroupParameters: this._qaseGroupParameters, @@ -214,6 +229,7 @@ export class qase { this._qaseID = ''; this._qaseTitle = ''; + this._qaseSuite = ''; this._qaseFields = ''; this._qaseParameters = ''; this._qaseGroupParameters = ''; diff --git a/qase-testcafe/src/reporter.ts b/qase-testcafe/src/reporter.ts index fbd880be..2fd20401 100644 --- a/qase-testcafe/src/reporter.ts +++ b/qase-testcafe/src/reporter.ts @@ -55,6 +55,7 @@ interface FixtureType { enum metadataEnum { id = 'QaseID', title = 'QaseTitle', + suite = 'QaseSuite', fields = 'QaseFields', parameters = 'QaseParameters', groupParameters = 'QaseGroupParameters', @@ -66,6 +67,7 @@ enum metadataEnum { interface MetadataType { [metadataEnum.id]: number[]; [metadataEnum.title]: string | undefined; + [metadataEnum.suite]: string | undefined; [metadataEnum.fields]: Record; [metadataEnum.parameters]: Record; [metadataEnum.groupParameters]: Record; @@ -239,12 +241,17 @@ export class TestcafeQaseReporter { group_params: metadata[metadataEnum.groupParameters], relations: { suite: { - data: [ - { - title: testRunInfo.fixture.name, - public_id: null, - }, - ], + data: metadata[metadataEnum.suite] + ? metadata[metadataEnum.suite].split('\t').map((s) => ({ + title: s, + public_id: null, + })) + : [ + { + title: testRunInfo.fixture.name, + public_id: null, + }, + ], }, }, run_id: null, @@ -271,6 +278,7 @@ export class TestcafeQaseReporter { const metadata: MetadataType = { QaseID: [], QaseTitle: undefined, + QaseSuite: undefined, QaseFields: {}, QaseParameters: {}, QaseGroupParameters: {}, @@ -292,6 +300,10 @@ export class TestcafeQaseReporter { metadata.QaseTitle = meta[metadataEnum.title]; } + if (meta[metadataEnum.suite] !== undefined && meta[metadataEnum.suite] !== '') { + metadata.QaseSuite = meta[metadataEnum.suite]; + } + if (meta[metadataEnum.fields] !== undefined && meta[metadataEnum.fields] !== '') { metadata.QaseFields = JSON.parse(meta[metadataEnum.fields]) as Record; } diff --git a/qase-testcafe/test/reporter.test.ts b/qase-testcafe/test/reporter.test.ts index f1c66542..2accc4b5 100644 --- a/qase-testcafe/test/reporter.test.ts +++ b/qase-testcafe/test/reporter.test.ts @@ -141,6 +141,24 @@ describe('TestcafeQaseReporter', () => { expect(reporterMock.addTestResult).not.toHaveBeenCalled(); }); + it('should use suite metadata for relations when QaseSuite is provided', async () => { + const suitesMeta = { ...meta, QaseSuite: 'Parent\tChild' }; + await reporter.reportTestDone('Test Title', testRunInfo, suitesMeta, formatError); + const call = reporterMock.addTestResult.mock.calls[0][0]; + expect(call.relations.suite.data).toEqual([ + { title: 'Parent', public_id: null }, + { title: 'Child', public_id: null }, + ]); + }); + + it('should fall back to fixture name when QaseSuite is not provided', async () => { + await reporter.reportTestDone('Test Title', testRunInfo, meta, formatError); + const call = reporterMock.addTestResult.mock.calls[0][0]; + expect(call.relations.suite.data).toEqual([ + { title: 'Fixture', public_id: null }, + ]); + }); + it('should handle errors and attachments', async () => { const info = { ...testRunInfo, errs: [{ userAgent: '', @@ -184,6 +202,15 @@ describe('TestcafeQaseReporter', () => { expect(result.QaseGroupParameters).toEqual({ g: 'v' }); expect(result.QaseIgnore).toBe(true); }); + it('should parse QaseSuite from meta', () => { + const meta = { QaseSuite: 'Parent\tChild' }; + const result = (reporter as any).getMeta(meta); + expect(result.QaseSuite).toBe('Parent\tChild'); + }); + it('should leave QaseSuite undefined when not provided', () => { + const result = (reporter as any).getMeta({}); + expect(result.QaseSuite).toBeUndefined(); + }); it('should handle empty meta', () => { const result = (reporter as any).getMeta({}); expect(result.QaseID).toEqual([]); diff --git a/qase-wdio/changelog.md b/qase-wdio/changelog.md index db41b7ad..74275875 100644 --- a/qase-wdio/changelog.md +++ b/qase-wdio/changelog.md @@ -1,3 +1,16 @@ +# qase-wdio@1.2.1 + +## What's new + +- Added `qase.comment()` method to set a comment for the test case. The comment is included in the test result message, prepended before any error message. + +```ts +it('should work', () => { + qase.comment('This test verifies login flow'); + // test code +}); +``` + # qase-wdio@1.2.0 ## What's new diff --git a/qase-wdio/package.json b/qase-wdio/package.json index e5abfbc5..93050499 100644 --- a/qase-wdio/package.json +++ b/qase-wdio/package.json @@ -1,6 +1,6 @@ { "name": "wdio-qase-reporter", - "version": "1.2.0", + "version": "1.2.1", "description": "Qase WebDriverIO Reporter", "homepage": "https://github.com/qase-tms/qase-javascript", "sideEffects": false, diff --git a/qase-wdio/src/events.ts b/qase-wdio/src/events.ts index f20d99f3..5a94cd95 100644 --- a/qase-wdio/src/events.ts +++ b/qase-wdio/src/events.ts @@ -6,6 +6,7 @@ export const events = { addIgnore: 'qase:ignore', addParameters: 'qase:parameters', addGroupParameters: 'qase:groupParameters', + addComment: 'qase:comment', addAttachment: 'qase:attachment', startStep: 'qase:startStep', endStep: 'qase:endStep', diff --git a/qase-wdio/src/models.ts b/qase-wdio/src/models.ts index 5ec98a03..12ced98b 100644 --- a/qase-wdio/src/models.ts +++ b/qase-wdio/src/models.ts @@ -14,6 +14,10 @@ export interface AddSuiteEventArgs { suite: string; } +export interface AddCommentEventArgs { + comment: string; +} + export interface AddAttachmentEventArgs { name?: string; type?: string; diff --git a/qase-wdio/src/reporter.ts b/qase-wdio/src/reporter.ts index 38dfd489..6de1ca2b 100644 --- a/qase-wdio/src/reporter.ts +++ b/qase-wdio/src/reporter.ts @@ -31,6 +31,7 @@ import { QaseReporterOptions } from './options'; import { isEmpty, isScreenshotCommand } from './utils'; import { AddAttachmentEventArgs, + AddCommentEventArgs, AddQaseIdEventArgs, AddRecordsEventArgs, AddSuiteEventArgs, @@ -320,10 +321,18 @@ export default class WDIOQaseReporter extends WDIOReporter { null : err.stacktrace === undefined ? null : err.stacktrace; - testResult.message = err === null ? + const errorMessage = err === null ? null : err.message === undefined ? null : err.message; + if (this.storage.comment) { + testResult.message = errorMessage + ? `${this.storage.comment}\n\n${errorMessage}` + : this.storage.comment; + } else { + testResult.message = errorMessage; + } + testResult.signature = generateSignature( Array.isArray(testResult.testops_id) ? testResult.testops_id : testResult.testops_id ? [testResult.testops_id] : null, [...this.storage.suites, testResult.title], @@ -393,6 +402,7 @@ export default class WDIOQaseReporter extends WDIOReporter { process.on(events.addSuite, this.addSuite.bind(this)); process.on(events.addParameters, this.addParameters.bind(this)); process.on(events.addGroupParameters, this.addGroupParameters.bind(this)); + process.on(events.addComment, this.addComment.bind(this)); process.on(events.addAttachment, this.addAttachment.bind(this)); process.on(events.addIgnore, this.ignore.bind(this)); process.on(events.addStep, this.addStep.bind(this)); @@ -416,6 +426,10 @@ export default class WDIOQaseReporter extends WDIOReporter { curTest.title = title; } + addComment({ comment }: AddCommentEventArgs) { + this.storage.comment = comment; + } + addSuite({ suite }: AddSuiteEventArgs) { const curTest = this.storage.getCurrentTest(); if (!curTest) { diff --git a/qase-wdio/src/storage.ts b/qase-wdio/src/storage.ts index 2e9ad537..4de71bbc 100644 --- a/qase-wdio/src/storage.ts +++ b/qase-wdio/src/storage.ts @@ -4,6 +4,7 @@ export class Storage { currentFile?: string | undefined; suites: string[] = []; ignore = false; + comment: string | undefined; items: (TestResultType | TestStepType)[] = []; @@ -11,6 +12,7 @@ export class Storage { this.currentFile = undefined; this.items = []; this.ignore = false; + this.comment = undefined; if (this.suites.length > 0) { this.suites.pop(); diff --git a/qase-wdio/src/wdio.ts b/qase-wdio/src/wdio.ts index 80fe1df0..82df4249 100644 --- a/qase-wdio/src/wdio.ts +++ b/qase-wdio/src/wdio.ts @@ -166,6 +166,22 @@ qase.suite = (value: string) => { return this; }; +/** + * Set a comment for the test case + * @param {string} value + * @example + * describe('suite', () => { + * it('should work', () => { + * qase.comment('Some comment'); + * // test code + * }); + * }); + */ +qase.comment = (value: string) => { + sendEvent(events.addComment, { comment: value }); + return this; +}; + /** * Set ignore for the test case * @example