Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ node_modules/
/localFiles

.idea/

# generated
src/generated/
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"lint": "eslint .",
"lint-fix": "eslint . --fix",
"format": "npx prettier --write .",
"generate:product-codes": "node scripts/generateProductCodes.mjs",
"pretest": "npm run generate:product-codes",
"test": "npx playwright test",
"ui": "npx playwright test --ui",
"report": "npx playwright show-report"
Expand Down
6 changes: 6 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import 'dotenv/config';

import { execSync } from 'node:child_process';

import { defineConfig, devices } from '@playwright/test';

import AppConfig from './src/config/AppConfig';

// Regenerate ProductCodes.generated.ts before any test files are loaded.
// Runs whether tests are invoked via `npm test` (pretest hook) or `npx playwright test` directly.
execSync('node scripts/generateProductCodes.mjs', { stdio: 'inherit' });

const appConfig = AppConfig.instance;

appConfig.initialize();
Expand Down
79 changes: 79 additions & 0 deletions scripts/generateProductCodes.mjs
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a fine solution for now, but by always using the same products for all tests, we can get into a situation where one test changes/deletes one of these products, causing all other tests to fail.

A better solution is likely to create the products that we need at the start of each test then delete the products at the end of the test. That way the tests are totally independent of each other.

But we can deal with that later

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, the products from CSV are imported during every single run, so we are sure that those products exist while testing

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const CSV_PATH = path.join(__dirname, '..', 'src', 'setup', 'dataImport', 'products.csv');
const OUTPUT_PATH = path.join(__dirname, '..', 'src', 'generated', 'ProductCodes.generated.ts');

// Stable keys for the generated objects, mapped to the name value
// in products.csv that identifies the row. The productCode (value) is read
// from the CSV at generation time, so codes can change without touching tests.
const PRODUCT_KEYS = {
ONE: 'E2E-product-one',
TWO: 'E2E-product-two',
THREE: 'E2E-product-three',
FOUR: 'E2E-product-four',
FIVE: 'E2E-product-five',
SIX: 'E2E-product-six',
};

function parseCsv(csv) {
const lines = csv.trim().split('\n');
if (lines.length < 2) {
return [];
}
const headers = lines[0].split(',').map((h) => h.trim());
const codeIndex = headers.indexOf('ProductCode');
const nameIndex = headers.indexOf('Name');
if (codeIndex === -1) {
throw new Error(`ProductCode column not found in ${CSV_PATH}`);
}
if (nameIndex === -1) {
throw new Error(`Name column not found in ${CSV_PATH}`);
}
return lines
.slice(1)
.map((line) => line.split(','))
.map((cols) => ({
code: cols[codeIndex]?.trim() ?? '',
name: cols[nameIndex]?.trim() ?? '',
}))
.filter((row) => row.code.length > 0);
}

function buildFileContent(rows) {
const codeByName = new Map(rows.map((row) => [row.name, row.code]));

const entries = Object.entries(PRODUCT_KEYS).map(([key, name]) => {
const code = codeByName.get(name);
if (!code) {
throw new Error(
`Product with Name "${name}" (key ${key}) not found in ${CSV_PATH}. ` +
'Update PRODUCT_KEYS in scripts/generateProductCodes.mjs or add the row to products.csv.'
);
}
return ` ${key}: '${code}',`;
});

return `// AUTO-GENERATED FILE. DO NOT EDIT.
// Regenerated from src/setup/dataImport/products.csv by scripts/generateProductCodes.mjs
// Output: src/generated/ProductCodes.generated.ts

export const Product = {
${entries.join('\n')}
} as const;

export type ProductCode = (typeof Product)[keyof typeof Product];
`;
}

const csv = fs.readFileSync(CSV_PATH, 'utf8');
const rows = parseCsv(csv);
if (rows.length === 0) {
throw new Error(`No product codes found in ${CSV_PATH}`);
}

fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });
fs.writeFileSync(OUTPUT_PATH, buildFileContent(rows), 'utf8');
7 changes: 3 additions & 4 deletions src/api/AuthService.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import BaseServiceModel from '@/api/BaseServiceModel';
import { CHOOSE_LOCATION, LOGIN_API } from '@/consts/apiUrls';

class AuthService extends BaseServiceModel {
async login(data: { username: string; password: string; location?: string }) {
const apiResponse = await this.request.get('./api/login', { data });
const apiResponse = await this.request.get(LOGIN_API, { data });
if (apiResponse.status() !== 200) {
throw new Error(`Authentication for user "${data.username}" failed`);
}
}

async changeLocation(locationId: string) {
const apiResponse = await this.request.put(
`./api/chooseLocation/${locationId}`
);
const apiResponse = await this.request.put(CHOOSE_LOCATION(locationId));
return await apiResponse.body();
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/api/GenericService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import BaseServiceModel from '@/api/BaseServiceModel';
import { APP_CONTEXT, GENERIC_USER_BY_ID } from '@/consts/apiUrls';
import { ApiResponse, AppContextResponse, User } from '@/types';
import { parseRequestToJSON } from '@/utils/ServiceUtils';

class GenericService extends BaseServiceModel {
async getAppContext(): Promise<ApiResponse<AppContextResponse>> {
try {
const apiResponse = await this.request.get('./api/getAppContext');
const apiResponse = await this.request.get(APP_CONTEXT);
return await parseRequestToJSON(apiResponse);
} catch (error) {
throw new Error('Problem fetching app context');
Expand All @@ -14,7 +15,7 @@ class GenericService extends BaseServiceModel {

async getUser(id: string): Promise<ApiResponse<User>> {
try {
const apiResponse = await this.request.get(`./api/generic/user/${id}`);
const apiResponse = await this.request.get(GENERIC_USER_BY_ID(id));
return await parseRequestToJSON(apiResponse);
} catch (error) {
throw new Error(`Problem fetching a user with id: ${id}`);
Expand Down
3 changes: 2 additions & 1 deletion src/api/InventoryService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import BaseServiceModel from '@/api/BaseServiceModel';
import { INVENTORY_IMPORT } from '@/consts/apiUrls';
import { jsonToCsv } from '@/utils/ServiceUtils';

class InventoryService extends BaseServiceModel {
async importInventories(data: Record<string, string>[], facilityId: string): Promise<void> {
try {
const csvContent = jsonToCsv(data);

const response = await this.request.post(`./api/facilities/${facilityId}/inventories/import`, {
const response = await this.request.post(INVENTORY_IMPORT(facilityId), {
data: csvContent,
headers: { 'Content-Type': 'text/csv' },
});
Expand Down
13 changes: 8 additions & 5 deletions src/api/LocationService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import BaseServiceModel from '@/api/BaseServiceModel';
import {
LOCATION_API,
LOCATION_BY_ID,
LOCATION_TYPES,
} from '@/consts/apiUrls';
import {
ApiResponse,
CreateLocationPayload,
Expand All @@ -10,7 +15,7 @@ import { parseRequestToJSON } from '@/utils/ServiceUtils';
class LocationService extends BaseServiceModel {
async getLocation(id: string): Promise<ApiResponse<LocationResponse>> {
try {
const apiResponse = await this.request.get(`./api/locations/${id}`);
const apiResponse = await this.request.get(LOCATION_BY_ID(id));
return await parseRequestToJSON(apiResponse);
} catch (error) {
throw new Error(`Problem fetching location with id: ${id}`);
Expand All @@ -22,7 +27,7 @@ class LocationService extends BaseServiceModel {
params = {}
): Promise<ApiResponse<LocationResponse>> {
try {
const apiResponse = await this.request.post('./api/locations', {
const apiResponse = await this.request.post(LOCATION_API, {
data: payload,
params,
});
Expand All @@ -34,9 +39,7 @@ class LocationService extends BaseServiceModel {

async getLocationTypes(): Promise<ApiResponse<LocationType[]>> {
try {
const apiResponse = await this.request.get(
'./api/locations/locationTypes'
);
const apiResponse = await this.request.get(LOCATION_TYPES);

return await parseRequestToJSON(apiResponse);
} catch (error) {
Expand Down
11 changes: 8 additions & 3 deletions src/api/ProductService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import BaseServiceModel from '@/api/BaseServiceModel';
import {
PRODUCT_BY_ID,
PRODUCT_DEMAND,
PRODUCT_IMPORT,
} from '@/consts/apiUrls';
import { ApiResponse, ProductDemandResponse, ProductResponse } from '@/types';
import { jsonToCsv, parseRequestToJSON } from '@/utils/ServiceUtils';

class ProductService extends BaseServiceModel {
async getDemand(id: string): Promise<ApiResponse<ProductDemandResponse>> {
try {
const apiResponse = await this.request.get(`./api/products/${id}/demand`);
const apiResponse = await this.request.get(PRODUCT_DEMAND(id));

return await parseRequestToJSON(apiResponse);
} catch (error) {
Expand All @@ -15,7 +20,7 @@ class ProductService extends BaseServiceModel {

async get(id: string): Promise<ApiResponse<ProductResponse>> {
try {
const apiResponse = await this.request.get(`./api/products/${id}`);
const apiResponse = await this.request.get(PRODUCT_BY_ID(id));

return await parseRequestToJSON(apiResponse);
} catch (error) {
Expand All @@ -28,7 +33,7 @@ class ProductService extends BaseServiceModel {
const csvContent = jsonToCsv(data);

const apiResponse = await this.request.post(
'./api/products/import',
PRODUCT_IMPORT,
{
data: csvContent,
headers: { 'Content-Type': 'text/csv' }
Expand Down
13 changes: 6 additions & 7 deletions src/api/ReceivingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import _ from 'lodash';

import BaseServiceModel from '@/api/BaseServiceModel';
import { PartialReceiptStatus } from '@/constants/PartialReceiptStatus';
import { PARTIAL_RECEIVING_BY_ID } from '@/consts/apiUrls';
import {
ApiResponse,
Container,
Expand All @@ -24,9 +25,7 @@ class ReceivingService extends BaseServiceModel {
*/
async getReceipt(id: string): Promise<ApiResponse<ReceiptResponse>> {
try {
const apiResponse = await this.request.get(
`./api/partialReceiving/${id}`
);
const apiResponse = await this.request.get(PARTIAL_RECEIVING_BY_ID(id));
return await parseRequestToJSON(apiResponse);
} catch (error) {
throw new Error('Problem fetching partial receipt');
Expand Down Expand Up @@ -94,7 +93,7 @@ class ReceivingService extends BaseServiceModel {
containers: containers,
recipient: receipt?.data?.recipient?.id,
};
await this.request.post(`./api/partialReceiving/${id}`, {
await this.request.post(PARTIAL_RECEIVING_BY_ID(id), {
data: payload,
});
} catch (error) {
Expand All @@ -116,7 +115,7 @@ class ReceivingService extends BaseServiceModel {
containers: this.createEmptyContainers(receipt?.containers),
recipient: receipt.recipient?.id,
};
await this.request.post(`./api/partialReceiving/${id}`, {
await this.request.post(PARTIAL_RECEIVING_BY_ID(id), {
data: payload,
});
} catch (error) {
Expand Down Expand Up @@ -267,7 +266,7 @@ class ReceivingService extends BaseServiceModel {
}

private async saveSplitLines(id: string, payload: ReceiptPayload) {
await this.request.post(`./api/partialReceiving/${id}`, {
await this.request.post(PARTIAL_RECEIVING_BY_ID(id), {
data: payload,
});
}
Expand All @@ -276,7 +275,7 @@ class ReceivingService extends BaseServiceModel {
id: string,
status: PartialReceiptStatus
): Promise<void> {
await this.request.post(`./api/partialReceiving/${id}`, {
await this.request.post(PARTIAL_RECEIVING_BY_ID(id), {
data: {
id,
stepNumber: 2,
Expand Down
19 changes: 13 additions & 6 deletions src/api/StockMovementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { APIRequestContext } from '@playwright/test';

import BaseServiceModel from '@/api/BaseServiceModel';
import { ShipmentType } from '@/constants/ShipmentType';
import {
STOCK_MOVEMENT_API,
STOCK_MOVEMENT_BY_ID,
STOCK_MOVEMENT_STATUS,
STOCK_MOVEMENT_UPDATE_ITEMS,
STOCK_MOVEMENT_UPDATE_SHIPMENT,
} from '@/consts/apiUrls';
import {
ApiResponse,
CreateInboundPayload,
Expand Down Expand Up @@ -33,7 +40,7 @@ class StockMovementService extends BaseServiceModel {
payload: CreateStockMovementPayload
): Promise<ApiResponse<StockMovementResponse>> {
try {
const apiResponse = await this.request.post('./api/stockMovements', {
const apiResponse = await this.request.post(STOCK_MOVEMENT_API, {
data: payload,
});

Expand All @@ -45,7 +52,7 @@ class StockMovementService extends BaseServiceModel {

async deleteStockMovement(id: string) {
try {
await this.request.delete(`./api/stockMovements/${id}`);
await this.request.delete(STOCK_MOVEMENT_BY_ID(id));
} catch (error) {
throw new Error('Problem deleting stock movement');
}
Expand All @@ -55,7 +62,7 @@ class StockMovementService extends BaseServiceModel {
id: string
): Promise<ApiResponse<StockMovementResponse>> {
try {
const apiResponse = await this.request.get(`./api/stockMovements/${id}`);
const apiResponse = await this.request.get(STOCK_MOVEMENT_BY_ID(id));
return await parseRequestToJSON(apiResponse);
} catch (error) {
throw new Error('Problem deleting stock movement');
Expand All @@ -68,7 +75,7 @@ class StockMovementService extends BaseServiceModel {
): Promise<ApiResponse<unknown>> {
try {
const apiResponse = await this.request.post(
`./api/stockMovements/${id}/updateItems`,
STOCK_MOVEMENT_UPDATE_ITEMS(id),
{
data: payload,
}
Expand All @@ -82,7 +89,7 @@ class StockMovementService extends BaseServiceModel {

async updateShipment(id: string, payload: UpdateStockMovementPayload) {
try {
await this.request.post(`./api/stockMovements/${id}/updateShipment`, {
await this.request.post(STOCK_MOVEMENT_UPDATE_SHIPMENT(id), {
data: payload,
});
} catch (error) {
Expand Down Expand Up @@ -112,7 +119,7 @@ class StockMovementService extends BaseServiceModel {
payload: UpdateStockMovementStatusPayload
) {
try {
await this.request.post(`./api/stockMovements/${id}/status`, {
await this.request.post(STOCK_MOVEMENT_STATUS(id), {
data: payload,
});
} catch (error) {
Expand Down
Loading
Loading