Skip to content

Commit d880c25

Browse files
committed
feat(#1354398): import export front - finalized e2e tests
1 parent 8b5c745 commit d880c25

File tree

6 files changed

+491
-2
lines changed

6 files changed

+491
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
id,name,type,is_active,scope_type,scope_codes,terms
2+
,Clothing Synonyms,synonym,true,localized_catalog,COM_FR;COM_EN,"dress,robe,gown;shirt,chemise,blouse;pants,pantalon,trousers"
3+
,Fashion Expansions,expansion,true,localized_catalog,COM_FR,"dress:robe,evening dress,cocktail dress;shoes:chaussures,sneakers,boots,sandals"
4+
,Electronics Synonyms,synonym,0,localized_catalog,COM_EN,"phone,smartphone,mobile;laptop,notebook,computer"
5+
,Size Expansions,expansion,1,locale,fr_FR,"small:petit,XS,S;large:grand,XL,XXL,XXXL"

front/e2e/src/helper/fileUpload.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Locator, Page, expect } from '@playwright/test'
2+
import { TestId, generateTestId } from './testIds'
3+
import path from 'node:path'
4+
5+
/**
6+
* Helper class for interacting with file upload dropzones in Playwright tests.
7+
*/
8+
export class FileUpload {
9+
private page: Page
10+
private resourceName: string
11+
private componentId: string
12+
13+
private dropzone: Locator
14+
private uploadButton: Locator
15+
private selectedFileName: Locator
16+
17+
/**
18+
* Creates a new FileUpload instance.
19+
*
20+
* @param page - The Playwright Page object
21+
* @param resourceName - The resource name used in the API endpoint (e.g., 'job_file')
22+
* @param componentId - The component ID used to generate data-testid for the dropzone
23+
*/
24+
constructor(page: Page, resourceName: string, componentId?: string) {
25+
this.page = page
26+
this.resourceName = resourceName
27+
this.componentId = componentId
28+
}
29+
30+
/**
31+
* Gets the dropzone locator (lazy initialization).
32+
*/
33+
private getDropzone(): Locator {
34+
if (!this.dropzone) {
35+
this.dropzone = this.page.getByTestId(
36+
generateTestId(TestId.FILE_UPLOAD_DROPZONE, this.componentId)
37+
)
38+
}
39+
return this.dropzone
40+
}
41+
42+
/**
43+
* Gets the upload button locator (lazy initialization).
44+
*/
45+
private getUploadButton(): Locator {
46+
if (!this.uploadButton) {
47+
const dropzone = this.getDropzone()
48+
this.uploadButton = dropzone.getByTestId(
49+
generateTestId(TestId.BUTTON, 'upload')
50+
)
51+
}
52+
return this.uploadButton
53+
}
54+
55+
/**
56+
* Gets the selected file name locator (lazy initialization).
57+
*/
58+
private getSelectedFileName(): Locator {
59+
if (!this.selectedFileName) {
60+
const dropzone = this.getDropzone()
61+
this.selectedFileName = dropzone.getByTestId(
62+
TestId.FILE_UPLOAD_DROPZONE_SELECTED_FILE_NAME
63+
)
64+
}
65+
return this.selectedFileName
66+
}
67+
68+
/**
69+
* Checks that the upload dropzone exists and is visible.
70+
*/
71+
public async expectDropzoneToExist(): Promise<void> {
72+
const dropzone = this.getDropzone()
73+
await expect(dropzone).toBeVisible()
74+
}
75+
76+
/**
77+
* Uploads a file by selecting it through the file chooser.
78+
*
79+
* @param filePath - Absolute or relative path to the file to upload
80+
* @param baseDir - Optional base directory (defaults to __dirname if filePath is relative)
81+
*/
82+
public async uploadFile(filePath: string, baseDir?: string): Promise<void> {
83+
const dropzone = this.getDropzone()
84+
const selectedFileName = this.getSelectedFileName()
85+
86+
// Resolve the full file path
87+
const fullPath = path.isAbsolute(filePath)
88+
? filePath
89+
: path.join(baseDir || __dirname, filePath)
90+
91+
// Extract just the filename for verification
92+
const fileName = path.basename(fullPath)
93+
94+
// Start waiting for file chooser before clicking
95+
const fileChooserPromise = this.page.waitForEvent('filechooser')
96+
await dropzone.click()
97+
const fileChooser = await fileChooserPromise
98+
await fileChooser.setFiles(fullPath)
99+
100+
// Verify the file name is displayed
101+
await expect(selectedFileName).toContainText(fileName)
102+
}
103+
104+
/**
105+
* Clicks the upload button and waits for the upload to complete successfully.
106+
*
107+
* @param onSuccess - Optional callback to execute after successful upload
108+
*/
109+
public async clickUploadAndWaitForSuccess(
110+
onSuccess?: () => Promise<void> | void
111+
): Promise<void> {
112+
const uploadButton = this.getUploadButton()
113+
114+
// Verify upload button is enabled
115+
await expect(uploadButton).not.toBeDisabled()
116+
117+
// Wait for the API response
118+
const uploadResponse = this.page.waitForResponse(
119+
`**/api/${this.resourceName}**`
120+
)
121+
122+
await uploadButton.click()
123+
await uploadResponse
124+
125+
// Execute the success callback if provided
126+
if (onSuccess) {
127+
await onSuccess()
128+
}
129+
}
130+
131+
/**
132+
* Expects the upload button to be visible.
133+
*/
134+
public async expectUploadButtonToBeVisible(): Promise<void> {
135+
const uploadButton = this.getUploadButton()
136+
await expect(uploadButton).toBeVisible()
137+
}
138+
139+
/**
140+
* Expects the upload button to be enabled.
141+
*/
142+
public async expectUploadButtonToBeEnabled(): Promise<void> {
143+
const uploadButton = this.getUploadButton()
144+
await expect(uploadButton).not.toBeDisabled()
145+
}
146+
147+
/**
148+
* Expects the selected file name to contain specific text.
149+
*
150+
* @param fileName - Expected file name text
151+
*/
152+
public async expectSelectedFileNameToContain(fileName: string): Promise<void> {
153+
const selectedFileName = this.getSelectedFileName()
154+
await expect(selectedFileName).toContainText(fileName)
155+
}
156+
}

front/e2e/src/helper/files.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import path from 'node:path'
2+
3+
export function getTestFilePath(relativePath: string): string {
4+
// Remove leading slash from relative path if present
5+
const normalizedPath = relativePath.startsWith('/')
6+
? relativePath.substring(1)
7+
: relativePath
8+
9+
// path.join automatically handles duplicate slashes and normalizes the path
10+
return path.join(__dirname, '../files', normalizedPath)
11+
}

front/e2e/src/helper/testIds.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export enum TestId {
8989
DIALOG_CANCEL_BUTTON = 'dialogCancelButton',
9090
DIALOG_CONFIRM_BUTTON = 'dialogConfirmButton',
9191
GRID_CREATE_BUTTON = 'gridCreateButton',
92+
GRID_JOB_BUTTON = 'gridJobButton',
9293
FIELD_SET = 'fieldSet',
9394
PREVIEW_REQUIRED_MESSAGE = 'previewRequiredMessage',
9495
RESOURCE_TABLE_NB_CUSTOM_VALUES_MESSAGE = 'resourceTableNbCustomValuesMessage',
@@ -108,13 +109,25 @@ export enum TestId {
108109
RULES_MANAGER = 'rulesManager',
109110
COMBINATION_RULES = 'combinationRules',
110111
CONFIGURATION_FORM = 'configurationForm',
112+
IMPORT_EXPORT_PROFILE_RUN = 'importExportProfileRun',
113+
LOGS = 'logs',
114+
JOBFILE = 'jobFile',
115+
JOBPROFILE = 'jobProfile',
116+
STATUS = 'status',
117+
UPLOAD_JOB_FILE_MODAL = 'uploadJobFileModal',
118+
UPLOAD_JOB_FILE_MODAL_TITLE = 'uploadJobFileModalTitle',
119+
FILE_UPLOAD_DROPZONE = 'fileUploadDropZone',
120+
FILE_UPLOAD_DROPZONE_SELECTED_FILE_NAME = 'selectedFileName',
121+
FILE_DOWNLOADER = 'fileDownloader',
111122
}
112123

113-
type ItemId = `|${string}` | ''
124+
export const TEST_ID_SEPARATOR = '|'
125+
126+
type ItemId = `${typeof TEST_ID_SEPARATOR}${string}` | ''
114127
export type FullTestId = `${TestId}${ItemId}`
115128

116129
function normalizeItemId(itemId?: string): ItemId {
117-
return itemId ? `|${itemId}` : ''
130+
return itemId ? `${TEST_ID_SEPARATOR}${itemId}` : ''
118131
}
119132

120133
export function generateTestId(testId: TestId, itemId?: string): FullTestId {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/* eslint-disable testing-library/prefer-screen-queries */
2+
import { Page, expect, test } from '@playwright/test'
3+
import { login } from '../../../../helper/auth'
4+
import { navigateTo } from '../../../../helper/menu'
5+
import { TestId, generateTestId } from '../../../../helper/testIds'
6+
import { Grid } from '../../../../helper/grid'
7+
import { Dropdown } from '../../../../helper/dropdown'
8+
import { Tabs } from '../../../../helper/tabs'
9+
10+
const jobResourceName = 'Job'
11+
12+
const texts = {
13+
labelMenuPage: 'Import / Export',
14+
jobProfileValue: 'Thesaurus export',
15+
profile: {
16+
thesaurusExport: 'Thesaurus export',
17+
},
18+
status: {
19+
new: 'New',
20+
},
21+
gridHeaders: {
22+
profile: 'Profile',
23+
status: 'Status',
24+
},
25+
tabs: {
26+
import: 'Import',
27+
export: 'Export',
28+
},
29+
}
30+
31+
interface IExportResourceConfig {
32+
name: string
33+
fileName?: string
34+
profileDisplayName?: string
35+
expectedStatus?: string
36+
modalTitle?: string
37+
}
38+
39+
const exportResourceConfigInputs: Record<string, IExportResourceConfig> = {
40+
thesaurus: { name: 'thesaurus' },
41+
}
42+
43+
function uppercaseFirstLetter(str: string): string {
44+
return str.charAt(0).toUpperCase() + str.slice(1)
45+
}
46+
47+
function getResourceConfig(name: string): Required<IExportResourceConfig> {
48+
const input = exportResourceConfigInputs[name]
49+
if (!input) {
50+
throw new Error(`No configuration found for resource: ${name}`)
51+
}
52+
53+
const displayName =
54+
input.profileDisplayName ?? `${uppercaseFirstLetter(name)} export`
55+
56+
return {
57+
name,
58+
fileName: input.fileName ?? `${name}.csv`,
59+
profileDisplayName: displayName,
60+
expectedStatus: input.expectedStatus ?? 'New',
61+
modalTitle: input.modalTitle ?? displayName,
62+
}
63+
}
64+
65+
async function testResourceExport(
66+
page: Page,
67+
resourceName: string
68+
): Promise<void> {
69+
const config = getResourceConfig(resourceName)
70+
await test.step('Login and navigate to the Exports page', async () => {
71+
await login(page)
72+
// Default page tab is import
73+
await navigateTo(page, texts.labelMenuPage, '/admin/importexport/import')
74+
// We then navigate to the export subtab
75+
const tabs = new Tabs(page)
76+
await tabs.expectToHaveTabs(Object.values(texts.tabs))
77+
await tabs.navigateTo(texts.tabs.export, '/admin/importexport/export')
78+
})
79+
80+
await test.step('Check profile list and run profile', async () => {
81+
const exportProfileDropdown = new Dropdown(page, generateTestId(TestId.JOBPROFILE, 'export'))
82+
await exportProfileDropdown.selectValue(config.profileDisplayName)
83+
84+
const createExportJobButton = page.getByTestId(
85+
generateTestId(TestId.IMPORT_EXPORT_PROFILE_RUN, 'export')
86+
)
87+
await expect(createExportJobButton).toBeDefined()
88+
await createExportJobButton.click()
89+
})
90+
91+
await test.step('Check a new job was added to the list', async () => {
92+
const grid = new Grid(page, jobResourceName)
93+
await grid.expectToBeVisible()
94+
await grid.expectToFindLineWhere([
95+
{
96+
columnName: texts.gridHeaders.profile,
97+
value: config.profileDisplayName,
98+
},
99+
{
100+
columnName: texts.gridHeaders.status,
101+
value: config.expectedStatus,
102+
},
103+
])
104+
// We can't run crons that are necessary to handle downloadable file creations during e2e test
105+
// TODO: find a way to run them once and test that the file is generated and contains the expected content
106+
})
107+
}
108+
109+
const exportableResources = {
110+
premium: ['thesaurus'],
111+
// standard: ['source_field']
112+
}
113+
114+
test.describe('Pages > Import / Export > Export', {tag: ['@premium']}, () => {
115+
for (const resourceName of exportableResources.premium) {
116+
test(uppercaseFirstLetter(resourceName), async ({ page }) => {
117+
await testResourceExport(page, resourceName)
118+
})
119+
}
120+
})

0 commit comments

Comments
 (0)