Skip to content

Commit 788bc89

Browse files
committed
feat: add job creation E2E test and related page objects, enhancing testing coverage for employer job posting functionality
1 parent 23fb753 commit 788bc89

File tree

4 files changed

+496
-2
lines changed

4 files changed

+496
-2
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Job Post Test Fixtures
3+
*
4+
* Provides reusable data for creating job posts in E2E tests.
5+
*/
6+
7+
export interface JobPostData {
8+
title: string;
9+
description: string;
10+
remote: boolean;
11+
minSalary: number;
12+
maxSalary: number;
13+
contactEmail: string;
14+
inclusiveOpportunity: boolean;
15+
}
16+
17+
/**
18+
* Generate unique job title with timestamp
19+
*/
20+
export function generateUniqueJobTitle(prefix: string = 'E2E Job'): string {
21+
const timestamp = Date.now();
22+
return `${prefix} ${timestamp}`;
23+
}
24+
25+
/**
26+
* Default job post data for testing
27+
*/
28+
export function getDefaultJobPostData(titlePrefix: string = 'E2E Job Post'): JobPostData {
29+
return {
30+
title: generateUniqueJobTitle(titlePrefix),
31+
description:
32+
'Automated E2E job post created for test coverage. This role validates the creation flow and should not be used for production hiring.',
33+
remote: true,
34+
minSalary: 60000,
35+
maxSalary: 90000,
36+
contactEmail: '[email protected]',
37+
inclusiveOpportunity: true,
38+
};
39+
}
40+
41+
/**
42+
* Generate custom job post data
43+
*/
44+
export function generateJobPostData(overrides: Partial<JobPostData> = {}): JobPostData {
45+
return {
46+
...getDefaultJobPostData(),
47+
...overrides,
48+
};
49+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/**
2+
* Create Job Page Object
3+
*
4+
* Page object for the job creation modal/form.
5+
*/
6+
7+
import { WebDriver, By } from 'selenium-webdriver';
8+
import { BasePage } from './BasePage.ts';
9+
import type { JobPostData } from '../fixtures/job.fixtures.ts';
10+
11+
export class CreateJobPage extends BasePage {
12+
// Modal container
13+
private readonly modalDialog = By.css('[role="dialog"][data-slot="dialog-content"]');
14+
15+
// Workplace selector (dropdown trigger and menu items)
16+
private readonly workplaceDropdownTrigger = By.css(
17+
'[role="dialog"] button[aria-haspopup="menu"], [role="dialog"] button[role="combobox"]'
18+
);
19+
private readonly workplaceMenuItems = By.css(
20+
'[role="menu"] [role="menuitem"], [role="menu"] [role="menuitemradio"], [role="menu"] [role="menuitemcheckbox"]'
21+
);
22+
23+
// Form field locators - scoped within the modal dialog
24+
private readonly titleInput = By.css('[role="dialog"] input[id="title"]');
25+
private readonly descriptionTextarea = By.css('[role="dialog"] textarea[id="description"]');
26+
private readonly remoteCheckbox = By.css('[role="dialog"] [id="remote"]');
27+
private readonly minSalaryInput = By.css('[role="dialog"] input[id="minSalary"]');
28+
private readonly maxSalaryInput = By.css('[role="dialog"] input[id="maxSalary"]');
29+
private readonly contactEmailInput = By.css('[role="dialog"] input[id="contactEmail"]');
30+
private readonly inclusiveOpportunityCheckbox = By.css(
31+
'[role="dialog"] [id="inclusiveOpportunity"]'
32+
);
33+
private readonly submitButton = By.css('[role="dialog"] button[type="submit"]');
34+
35+
constructor(driver: WebDriver) {
36+
super(driver);
37+
}
38+
39+
/**
40+
* Wait for the modal to be visible
41+
*/
42+
async waitForModal(timeout: number = 8000): Promise<void> {
43+
await this.waitForVisible(this.modalDialog, timeout);
44+
}
45+
46+
/**
47+
* Select the first workplace from the dropdown
48+
*/
49+
async selectFirstWorkplace(): Promise<void> {
50+
await this.openWorkplaceDropdown();
51+
52+
const selectors = [
53+
this.workplaceMenuItems,
54+
By.css('[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"]'),
55+
];
56+
57+
for (const selector of selectors) {
58+
try {
59+
await this.waitForElement(selector, 5000);
60+
const items = await this.findElements(selector);
61+
if (items.length > 0) {
62+
await items[0].click();
63+
await this.sleep(300);
64+
return;
65+
}
66+
} catch (error) {
67+
continue;
68+
}
69+
}
70+
71+
throw new Error('Could not select workplace - no options found');
72+
}
73+
74+
/**
75+
* Fill job title
76+
*/
77+
async fillTitle(title: string): Promise<void> {
78+
await this.tryMultipleSelectors(
79+
[
80+
this.titleInput,
81+
By.css('[role="dialog"] input[placeholder*="title" i]'),
82+
],
83+
async (locator) => await this.type(locator, title)
84+
);
85+
}
86+
87+
/**
88+
* Fill job description
89+
*/
90+
async fillDescription(description: string): Promise<void> {
91+
await this.tryMultipleSelectors(
92+
[
93+
this.descriptionTextarea,
94+
By.css('[role="dialog"] textarea[placeholder*="description" i]'),
95+
],
96+
async (locator) => await this.type(locator, description)
97+
);
98+
}
99+
100+
/**
101+
* Set remote checkbox
102+
*/
103+
async setRemote(remote: boolean): Promise<void> {
104+
await this.setCheckboxState(this.remoteCheckbox, remote);
105+
}
106+
107+
/**
108+
* Fill salary range
109+
*/
110+
async fillSalaryRange(minSalary: number, maxSalary: number): Promise<void> {
111+
await this.type(this.minSalaryInput, minSalary.toString());
112+
await this.type(this.maxSalaryInput, maxSalary.toString());
113+
}
114+
115+
/**
116+
* Fill contact email
117+
*/
118+
async fillContactEmail(email: string): Promise<void> {
119+
await this.type(this.contactEmailInput, email);
120+
}
121+
122+
/**
123+
* Set inclusive opportunity checkbox
124+
*/
125+
async setInclusiveOpportunity(enabled: boolean): Promise<void> {
126+
await this.setCheckboxState(this.inclusiveOpportunityCheckbox, enabled);
127+
}
128+
129+
/**
130+
* Fill full job form
131+
*/
132+
async fillJobForm(data: JobPostData): Promise<void> {
133+
await this.waitForModal();
134+
await this.selectFirstWorkplace();
135+
await this.fillTitle(data.title);
136+
await this.fillDescription(data.description);
137+
await this.setRemote(data.remote);
138+
await this.fillSalaryRange(data.minSalary, data.maxSalary);
139+
await this.fillContactEmail(data.contactEmail);
140+
await this.setInclusiveOpportunity(data.inclusiveOpportunity);
141+
}
142+
143+
/**
144+
* Submit the job form
145+
*/
146+
async submit(): Promise<void> {
147+
await this.tryMultipleSelectors(
148+
[
149+
this.submitButton,
150+
By.xpath('//*[@role="dialog"]//button[@type="submit"]'),
151+
],
152+
async (locator) => await this.click(locator)
153+
);
154+
}
155+
156+
/**
157+
* Wait for success (modal closes after submission)
158+
*/
159+
async waitForSuccess(timeout: number = 10000): Promise<void> {
160+
await this.waitForElementToDisappear(this.modalDialog, timeout);
161+
}
162+
163+
/**
164+
* Open workplace dropdown
165+
*/
166+
private async openWorkplaceDropdown(): Promise<void> {
167+
const triggers = [
168+
this.workplaceDropdownTrigger,
169+
By.xpath('//*[@role="dialog"]//button[contains(@aria-haspopup, "menu")]'),
170+
By.xpath('//*[@role="dialog"]//button[contains(@class, "justify-between")]'),
171+
];
172+
173+
await this.tryMultipleSelectors(triggers, async (locator) => {
174+
await this.click(locator);
175+
await this.sleep(300);
176+
});
177+
}
178+
179+
/**
180+
* Helper: set checkbox/radix toggle to desired state
181+
*/
182+
private async setCheckboxState(locator: By, shouldBeChecked: boolean): Promise<void> {
183+
const element = await this.waitForElement(locator, 5000);
184+
const ariaChecked = await element.getAttribute('aria-checked');
185+
const dataState = await element.getAttribute('data-state');
186+
const isChecked = ariaChecked === 'true' || dataState === 'checked';
187+
188+
if (isChecked !== shouldBeChecked) {
189+
await element.click();
190+
await this.sleep(200);
191+
}
192+
}
193+
194+
/**
195+
* Helper: Try multiple selectors until one works
196+
*/
197+
private async tryMultipleSelectors(
198+
selectors: By[],
199+
action: (locator: By) => Promise<void>
200+
): Promise<void> {
201+
for (const selector of selectors) {
202+
try {
203+
const exists = await this.elementExists(selector);
204+
if (exists) {
205+
await action(selector);
206+
return;
207+
}
208+
} catch (error) {
209+
continue;
210+
}
211+
}
212+
213+
throw new Error('None of the selectors worked for action');
214+
}
215+
216+
/**
217+
* Helper: Wait for element to disappear
218+
*/
219+
private async waitForElementToDisappear(locator: By, timeout: number): Promise<void> {
220+
const endTime = Date.now() + timeout;
221+
222+
while (Date.now() < endTime) {
223+
try {
224+
const exists = await this.elementExists(locator);
225+
if (!exists) {
226+
return;
227+
}
228+
await this.sleep(100);
229+
} catch (error) {
230+
return;
231+
}
232+
}
233+
234+
throw new Error('Element did not disappear within timeout');
235+
}
236+
}

apps/jobboard-frontend/tests/e2e/selenium/pages/EmployerDashboardPage.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export class EmployerDashboardPage extends BasePage {
1212
private readonly createWorkplaceButton = By.css(
1313
'button[aria-label*="Create"], button:has-text("Create Workplace"), [data-testid="create-workplace-btn"]'
1414
);
15+
private readonly createJobButton = By.xpath(
16+
'//button[contains(., "Create Job") or contains(., "createJob")]'
17+
);
1518
private readonly workplaceCards = By.css('[data-testid="workplace-card"], .workplace-card');
1619
private readonly workplaceModal = By.css('[role="dialog"], .modal');
1720

@@ -31,10 +34,11 @@ export class EmployerDashboardPage extends BasePage {
3134
* Click create workplace button
3235
*/
3336
async clickCreateWorkplace(): Promise<void> {
34-
// Try multiple selectors for flexibility
3537
const possibleSelectors = [
38+
// When workplaces exist - "New Workplace" button with Plus icon
39+
By.xpath('//button[contains(., "New Workplace") or contains(., "newWorkplace")]'),
40+
By.css('button:has(svg) + button:has(svg)'), // The first button in the workplace actions
3641
this.createWorkplaceButton,
37-
By.css('a[href*="create"]'),
3842
];
3943

4044
for (const selector of possibleSelectors) {
@@ -53,6 +57,31 @@ export class EmployerDashboardPage extends BasePage {
5357
throw new Error('Create workplace button not found');
5458
}
5559

60+
/**
61+
* Click create job button (global or within workplace)
62+
*/
63+
async clickCreateJob(): Promise<void> {
64+
const possibleSelectors = [
65+
this.createJobButton,
66+
By.xpath('//button[contains(., "Post New Job") or contains(., "post new job")]'),
67+
By.css('[data-testid="create-job-btn"]'),
68+
];
69+
70+
for (const selector of possibleSelectors) {
71+
try {
72+
const exists = await this.elementExists(selector);
73+
if (exists) {
74+
await this.click(selector);
75+
return;
76+
}
77+
} catch (error) {
78+
continue;
79+
}
80+
}
81+
82+
throw new Error('Create job button not found');
83+
}
84+
5685
/**
5786
* Wait for create workplace modal to appear
5887
*/
@@ -95,4 +124,27 @@ export class EmployerDashboardPage extends BasePage {
95124
return false;
96125
}
97126
}
127+
128+
/**
129+
* Find job posting by title on dashboard tables
130+
*/
131+
async findJobByTitle(title: string): Promise<boolean> {
132+
const selectors = [
133+
By.xpath(`//table//div[contains(@class, "font-semibold") and contains(., "${title}")]`),
134+
By.xpath(`//table//td[contains(., "${title}")]`),
135+
By.xpath(`//*[contains(text(), "${title}") and ancestor::table]`),
136+
];
137+
138+
for (const selector of selectors) {
139+
try {
140+
if (await this.elementExists(selector)) {
141+
return true;
142+
}
143+
} catch (error) {
144+
continue;
145+
}
146+
}
147+
148+
return false;
149+
}
98150
}

0 commit comments

Comments
 (0)