Skip to content

Instantly share code, notes, and snippets.

@wellwind
Last active April 8, 2025 13:41
Show Gist options
  • Save wellwind/ccf2abb3208e15e4638861da9eb163e7 to your computer and use it in GitHub Desktop.
Save wellwind/ccf2abb3208e15e4638861da9eb163e7 to your computer and use it in GitHub Desktop.
import { browser, Page } from 'k6/browser';
import { check } from 'k6';
import { Trend } from 'k6/metrics';
// --- Options Configuration ---
export const options = {
scenarios: {
ui_testing: {
executor: 'per-vu-iterations', // Specify the executor type
vus: 3,
iterations: 5,
options: {
browser: {
type: 'chromium',
},
},
},
},
// Thresholds - Automatic PASS/FAIL determination for test results
thresholds: {
checks: ['rate==1.0'], // All checks must pass
step1_goto_duration: ['p(95)<=2000'], // 95th percentile of step 1 duration <= 2000ms
step2_click_duration: ['p(95)<=500'], // 95th percentile of step 2 duration <= 500ms
step3_check_tag_duration: ['p(95)<=500'], // 95th percentile of step 3 duration <= 500ms
browser_web_vital_ttfb: ['p(95) < 1000'], // 95th percentile of TTFB < 1000ms
},
};
// --- Custom Time Series Metrics (Trends) ---
// Create Trend metrics to track the duration of each step
const gotoTiming = new Trend('step1_goto_duration', true);
const clickTiming = new Trend('step2_click_duration', true);
const checkTagTiming = new Trend('step3_check_tag_duration', true);
/**
* BasePage class that provides common functionality for all page objects
*/
class BasePage {
protected page: Page;
protected vu: number;
protected iter: number;
constructor(page: Page, vu: number, iter: number) {
this.page = page;
this.vu = vu;
this.iter = iter;
}
/**
* Logs a message with the current VU and iteration context
*/
protected log(message: string): void {
console.log(`VU ${this.vu}, Iter ${this.iter}: ${message}`);
}
/**
* Logs an error with the current VU and iteration context
*/
protected logError(message: string): void {
console.error(`VU ${this.vu}, Iter ${this.iter}: ${message}`);
}
/**
* Measures a performance operation with proper error handling
* @param operationName Name of the operation (used for performance marks)
* @param operation The async function to measure
* @param timingMetric The trend metric to record the timing
* @param fallbackDuration Duration to use if timing fails but operation succeeds
* @param timeoutDuration Duration to use if operation fails
*/
protected async measureOperation(
operationName: string,
operation: () => Promise<void>,
timingMetric: Trend,
fallbackDuration: number,
timeoutDuration: number,
): Promise<boolean> {
let success = false;
try {
// Set start mark with error handling
try {
await this.page.evaluate(
(name) => window.performance.mark(`${name}_start`),
operationName,
);
} catch (markError) {
this.log(
`Unable to set ${operationName} start mark: ${markError.message}`,
);
}
// Execute the operation
await operation();
// Try to measure the performance
let duration;
try {
await this.page.evaluate(
(name) => window.performance.mark(`${name}_end`),
operationName,
);
await this.page.evaluate(
(name) =>
window.performance.measure(
`${name}_duration`,
`${name}_start`,
`${name}_end`,
),
operationName,
);
duration = await this.page.evaluate((name) => {
const entries = window.performance.getEntriesByName(
`${name}_duration`,
);
if (entries && entries.length > 0) {
return entries[0].duration;
}
return null;
}, operationName);
} catch (timingError) {
this.log(
`${operationName} performance timing failed: ${timingError.message}`,
);
}
// Use the measured duration or fallback to a reasonable estimate
if (duration) {
timingMetric.add(duration);
} else {
timingMetric.add(fallbackDuration);
}
success = true;
} catch (e) {
// Use timeout duration for failures
timingMetric.add(timeoutDuration);
this.logError(
`${operationName} FAILED: ${e ? e.message : 'Unknown error'}`,
);
}
return success;
}
}
/**
* BlogPage represents the blog homepage and its interactions
*/
class BlogPage extends BasePage {
// Page URL
private readonly url = 'https://fullstackladder.dev/blog/';
constructor(page: Page, vu: number, iter: number) {
super(page, vu, iter);
}
/**
* Navigate to the blog page
*/
async navigateTo(): Promise<boolean> {
this.log('Step 1 - Navigating to blog page');
const success = await this.measureOperation(
'step1',
async () => {
// Set navigation timeout
this.page.setDefaultNavigationTimeout(30000);
await this.page.goto(this.url, {
waitUntil: 'domcontentloaded',
});
},
gotoTiming,
1000, // Fallback duration if timing fails but operation succeeds
30000, // Timeout duration for failures
);
if (success) {
this.log('Step 1 - Navigation successful');
}
check(success, { 'Step 1: Page loaded successfully': (s) => s });
return success;
}
/**
* Click on the Tags link in the navigation
*/
async clickTagsLink(): Promise<boolean> {
this.log("Step 2 - Finding and clicking the 'Tags' link");
const success = await this.measureOperation(
'step2',
async () => {
// Use XPath locator to find the <a> element containing <span class="link-text">標籤</span>
const linkLocator = this.page.locator(
'//a[.//span[@class="link-text" and normalize-space()="標籤"]]',
);
// Click the element with an operation timeout
await linkLocator.click({ timeout: 1000 });
},
clickTiming,
500, // Fallback duration if timing fails but operation succeeds
1000, // Timeout duration for failures
);
if (success) {
this.log('Step 2 - Click successful');
}
check(success, { 'Step 2: Successfully clicked "Tags" link': (s) => s });
return success;
}
/**
* Verify that the blog tags element is visible
*/
async verifyTagsElementVisible(): Promise<boolean> {
this.log("Step 3 - Verifying 'app-blog-tags' element exists");
const success = await this.measureOperation(
'step3',
async () => {
await this.page
.locator('app-blog-tags')
.waitFor({ state: 'visible', timeout: 2000 });
},
checkTagTiming,
200, // Fallback duration if timing fails but operation succeeds
2000, // Timeout duration for failures
);
if (success) {
this.log("Step 3 - Element 'app-blog-tags' is visible");
}
check(success, { 'Step 3: app-blog-tags element is visible': (s) => s });
return success;
}
}
// --- Main Test Logic (default function) ---
export default async function () {
// Create a new browser page for each iteration of each VU
const page = await browser.newPage();
// Set default timeout for page operations (ms) to prevent hangs
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
const vu = __VU; // Virtual User ID
const iter = __ITER; // Iteration number for the current VU
console.log(`VU ${vu}, Iter ${iter}: Starting iteration`);
try {
// Create blog page object
const blogPage = new BlogPage(page, vu, iter);
let stepSuccess: boolean;
// Step 1: Navigate to blog page
stepSuccess = await blogPage.navigateTo();
if (!stepSuccess) throw new Error('Step 1 Failed, stopping iteration');
// Step 2: Click on tags link
stepSuccess = await blogPage.clickTagsLink();
if (!stepSuccess) throw new Error('Step 2 Failed, stopping iteration');
// Step 3: Verify tags element is visible
stepSuccess = await blogPage.verifyTagsElementVisible();
if (!stepSuccess) throw new Error('Step 3 Failed, stopping iteration');
console.log(`VU ${vu}, Iter ${iter}: Iteration finished successfully`);
} catch (err: any) {
// Catch errors thrown from steps
console.error(
`VU ${vu}, Iter ${iter}: Iteration aborted due to error: ${err.message}`,
);
// Optionally record a failed check if the iteration is aborted
check(false, { 'Iteration Overall Success': (ok) => ok });
} finally {
// --- Cleanup: Close the page regardless of success or failure ---
await page.close();
console.log(`VU ${vu}, Iter ${iter}: Page closed`);
}
}
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: tag-cloud.k6.ts
output: -
scenarios: (100.00%) 1 scenario, 3 max VUs, 10m30s max duration (incl. graceful stop):
* ui_testing: 5 iterations for each of 3 VUs (maxDuration: 10m0s, gracefulStop: 30s)
INFO[0000] VU 3, Iter 0: Starting iteration source=console
INFO[0000] VU 3, Iter 0: Step 1 - Navigating to blog page source=console
INFO[0000] VU 2, Iter 0: Starting iteration source=console
INFO[0000] VU 2, Iter 0: Step 1 - Navigating to blog page source=console
INFO[0000] VU 1, Iter 0: Starting iteration source=console
INFO[0000] VU 1, Iter 0: Step 1 - Navigating to blog page source=console
INFO[0002] VU 2, Iter 0: step1 performance timing failed: undefined source=console
INFO[0002] VU 2, Iter 0: Step 1 - Navigation successful source=console
INFO[0002] VU 2, Iter 0: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0002] VU 2, Iter 0: Step 2 - Click successful source=console
INFO[0002] VU 2, Iter 0: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0003] VU 2, Iter 0: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0003] VU 2, Iter 0: Iteration finished successfully source=console
INFO[0003] VU 2, Iter 0: Page closed source=console
INFO[0003] VU 3, Iter 0: step1 performance timing failed: undefined source=console
INFO[0003] VU 3, Iter 0: Step 1 - Navigation successful source=console
INFO[0003] VU 3, Iter 0: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0003] VU 1, Iter 0: step1 performance timing failed: undefined source=console
INFO[0003] VU 1, Iter 0: Step 1 - Navigation successful source=console
INFO[0003] VU 1, Iter 0: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0003] VU 3, Iter 0: Step 2 - Click successful source=console
INFO[0003] VU 3, Iter 0: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0003] VU 2, Iter 1: Starting iteration source=console
INFO[0003] VU 2, Iter 1: Step 1 - Navigating to blog page source=console
INFO[0003] VU 1, Iter 0: Step 2 - Click successful source=console
INFO[0003] VU 1, Iter 0: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0004] VU 3, Iter 0: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0004] VU 3, Iter 0: Iteration finished successfully source=console
INFO[0004] VU 3, Iter 0: Page closed source=console
INFO[0004] VU 1, Iter 0: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0004] VU 1, Iter 0: Iteration finished successfully source=console
INFO[0004] VU 1, Iter 0: Page closed source=console
INFO[0004] VU 3, Iter 1: Starting iteration source=console
INFO[0004] VU 3, Iter 1: Step 1 - Navigating to blog page source=console
INFO[0005] VU 1, Iter 1: Starting iteration source=console
INFO[0005] VU 1, Iter 1: Step 1 - Navigating to blog page source=console
INFO[0005] VU 2, Iter 1: step1 performance timing failed: undefined source=console
INFO[0005] VU 2, Iter 1: Step 1 - Navigation successful source=console
INFO[0005] VU 2, Iter 1: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0005] VU 2, Iter 1: Step 2 - Click successful source=console
INFO[0005] VU 2, Iter 1: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0005] VU 2, Iter 1: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0005] VU 2, Iter 1: Iteration finished successfully source=console
INFO[0006] VU 2, Iter 1: Page closed source=console
INFO[0006] VU 3, Iter 1: step1 performance timing failed: undefined source=console
INFO[0006] VU 3, Iter 1: Step 1 - Navigation successful source=console
INFO[0006] VU 3, Iter 1: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0006] VU 1, Iter 1: step1 performance timing failed: undefined source=console
INFO[0006] VU 1, Iter 1: Step 1 - Navigation successful source=console
INFO[0006] VU 1, Iter 1: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0006] VU 2, Iter 2: Starting iteration source=console
INFO[0006] VU 2, Iter 2: Step 1 - Navigating to blog page source=console
INFO[0006] VU 3, Iter 1: Step 2 - Click successful source=console
INFO[0006] VU 3, Iter 1: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0006] VU 1, Iter 1: Step 2 - Click successful source=console
INFO[0006] VU 1, Iter 1: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0007] VU 3, Iter 1: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0007] VU 3, Iter 1: Iteration finished successfully source=console
INFO[0007] VU 3, Iter 1: Page closed source=console
INFO[0007] VU 1, Iter 1: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0007] VU 1, Iter 1: Iteration finished successfully source=console
INFO[0007] VU 1, Iter 1: Page closed source=console
INFO[0007] VU 3, Iter 2: Starting iteration source=console
INFO[0007] VU 3, Iter 2: Step 1 - Navigating to blog page source=console
INFO[0008] VU 1, Iter 2: Starting iteration source=console
INFO[0008] VU 1, Iter 2: Step 1 - Navigating to blog page source=console
INFO[0008] VU 2, Iter 2: step1 performance timing failed: undefined source=console
INFO[0008] VU 2, Iter 2: Step 1 - Navigation successful source=console
INFO[0008] VU 2, Iter 2: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0008] VU 2, Iter 2: Step 2 - Click successful source=console
INFO[0008] VU 2, Iter 2: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0009] VU 2, Iter 2: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0009] VU 2, Iter 2: Iteration finished successfully source=console
INFO[0009] VU 2, Iter 2: Page closed source=console
INFO[0009] VU 3, Iter 2: step1 performance timing failed: undefined source=console
INFO[0009] VU 3, Iter 2: Step 1 - Navigation successful source=console
INFO[0009] VU 3, Iter 2: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0009] VU 1, Iter 2: step1 performance timing failed: undefined source=console
INFO[0009] VU 1, Iter 2: Step 1 - Navigation successful source=console
INFO[0009] VU 1, Iter 2: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0009] VU 2, Iter 3: Starting iteration source=console
INFO[0009] VU 2, Iter 3: Step 1 - Navigating to blog page source=console
INFO[0009] VU 3, Iter 2: Step 2 - Click successful source=console
INFO[0009] VU 3, Iter 2: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0010] VU 1, Iter 2: Step 2 - Click successful source=console
INFO[0010] VU 1, Iter 2: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0010] VU 3, Iter 2: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0010] VU 3, Iter 2: Iteration finished successfully source=console
INFO[0010] VU 3, Iter 2: Page closed source=console
INFO[0010] VU 1, Iter 2: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0010] VU 1, Iter 2: Iteration finished successfully source=console
INFO[0010] VU 1, Iter 2: Page closed source=console
INFO[0011] VU 3, Iter 3: Starting iteration source=console
INFO[0011] VU 3, Iter 3: Step 1 - Navigating to blog page source=console
INFO[0011] VU 1, Iter 3: Starting iteration source=console
INFO[0011] VU 1, Iter 3: Step 1 - Navigating to blog page source=console
INFO[0011] VU 2, Iter 3: step1 performance timing failed: undefined source=console
INFO[0011] VU 2, Iter 3: Step 1 - Navigation successful source=console
INFO[0011] VU 2, Iter 3: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0011] VU 2, Iter 3: Step 2 - Click successful source=console
INFO[0011] VU 2, Iter 3: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0012] VU 2, Iter 3: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0012] VU 2, Iter 3: Iteration finished successfully source=console
INFO[0012] VU 2, Iter 3: Page closed source=console
INFO[0012] VU 3, Iter 3: step1 performance timing failed: undefined source=console
INFO[0012] VU 3, Iter 3: Step 1 - Navigation successful source=console
INFO[0012] VU 3, Iter 3: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0012] VU 1, Iter 3: step1 performance timing failed: undefined source=console
INFO[0012] VU 1, Iter 3: Step 1 - Navigation successful source=console
INFO[0012] VU 1, Iter 3: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0012] VU 2, Iter 4: Starting iteration source=console
INFO[0012] VU 2, Iter 4: Step 1 - Navigating to blog page source=console
INFO[0012] VU 3, Iter 3: Step 2 - Click successful source=console
INFO[0012] VU 3, Iter 3: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0013] VU 1, Iter 3: Step 2 - Click successful source=console
INFO[0013] VU 1, Iter 3: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0013] VU 3, Iter 3: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0013] VU 3, Iter 3: Iteration finished successfully source=console
INFO[0013] VU 3, Iter 3: Page closed source=console
INFO[0013] VU 1, Iter 3: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0013] VU 1, Iter 3: Iteration finished successfully source=console
INFO[0013] VU 1, Iter 3: Page closed source=console
INFO[0014] VU 3, Iter 4: Starting iteration source=console
INFO[0014] VU 3, Iter 4: Step 1 - Navigating to blog page source=console
INFO[0014] VU 2, Iter 4: step1 performance timing failed: undefined source=console
INFO[0014] VU 2, Iter 4: Step 1 - Navigation successful source=console
INFO[0014] VU 2, Iter 4: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0014] VU 1, Iter 4: Starting iteration source=console
INFO[0014] VU 1, Iter 4: Step 1 - Navigating to blog page source=console
INFO[0014] VU 2, Iter 4: Step 2 - Click successful source=console
INFO[0014] VU 2, Iter 4: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0015] VU 2, Iter 4: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0015] VU 2, Iter 4: Iteration finished successfully source=console
INFO[0015] VU 2, Iter 4: Page closed source=console
INFO[0015] VU 3, Iter 4: step1 performance timing failed: undefined source=console
INFO[0015] VU 3, Iter 4: Step 1 - Navigation successful source=console
INFO[0015] VU 3, Iter 4: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0015] VU 1, Iter 4: step1 performance timing failed: undefined source=console
INFO[0015] VU 1, Iter 4: Step 1 - Navigation successful source=console
INFO[0015] VU 1, Iter 4: Step 2 - Finding and clicking the 'Tags' link source=console
INFO[0015] VU 3, Iter 4: Step 2 - Click successful source=console
INFO[0015] VU 3, Iter 4: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0016] VU 1, Iter 4: Step 2 - Click successful source=console
INFO[0016] VU 1, Iter 4: Step 3 - Verifying 'app-blog-tags' element exists source=console
INFO[0016] VU 3, Iter 4: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0016] VU 3, Iter 4: Iteration finished successfully source=console
INFO[0016] VU 3, Iter 4: Page closed source=console
INFO[0016] VU 1, Iter 4: Step 3 - Element 'app-blog-tags' is visible source=console
INFO[0016] VU 1, Iter 4: Iteration finished successfully source=console
INFO[0016] VU 1, Iter 4: Page closed source=console
█ THRESHOLDS
browser_web_vital_ttfb
✓ 'p(95) < 1000' p(95)=509.27ms
checks
✓ 'rate==1.0' rate=100.00%
step1_goto_duration
✓ 'p(95)<=2000' p(95)=1s
step2_click_duration
✓ 'p(95)<=500' p(95)=328.24ms
step3_check_tag_duration
✓ 'p(95)<=500' p(95)=279.34ms
█ TOTAL RESULTS
checks_total.......................: 45 2.714756/s
checks_succeeded...................: 100.00% 45 out of 45
checks_failed......................: 0.00% 0 out of 45
✓ Step 1: Page loaded successfully
✓ Step 2: Successfully clicked "Tags" link
✓ Step 3: app-blog-tags element is visible
CUSTOM
step1_goto_duration..................................: avg=1s min=1s med=1s max=1s p(90)=1s p(95)=1s
step2_click_duration.................................: avg=311.08ms min=300.59ms med=309.2ms max=339.09ms p(90)=320.11ms p(95)=328.24ms
step3_check_tag_duration.............................: avg=226.53ms min=205.8ms med=214.5ms max=387ms p(90)=230.64ms p(95)=279.34ms
EXECUTION
iteration_duration...................................: avg=2.68s min=2.21s med=2.55s max=3.68s p(90)=3.22s p(95)=3.55s
iterations...........................................: 15 0.904919/s
vus..................................................: 2 min=2 max=3
vus_max..............................................: 3 min=3 max=3
NETWORK
data_received........................................: 0 B 0 B/s
data_sent............................................: 0 B 0 B/s
BROWSER
browser_data_received................................: 38 MB 2.3 MB/s
browser_data_sent....................................: 343 kB 21 kB/s
browser_http_req_duration............................: avg=242.69ms min=468µs med=167.25ms max=1.63s p(90)=457.22ms p(95)=504.16ms
browser_http_req_failed..............................: 0.00% 0 out of 648
WEB_VITALS
browser_web_vital_cls................................: avg=0.000797 min=0.000797 med=0.000797 max=0.000797 p(90)=0.000797 p(95)=0.000797
browser_web_vital_fcp................................: avg=691.73ms min=568ms med=700ms max=840ms p(90)=745.6ms p(95)=778.4ms
browser_web_vital_fid................................: avg=273.33µs min=199.99µs med=299.99µs max=400µs p(90)=359.99µs p(95)=399.99µs
browser_web_vital_inp................................: avg=36.26ms min=32ms med=32ms max=56ms p(90)=40ms p(95)=44.79ms
browser_web_vital_lcp................................: avg=1.38s min=968ms med=1.22s max=2.4s p(90)=1.9s p(95)=2.28s
browser_web_vital_ttfb...............................: avg=369.46ms min=326.09ms med=348ms max=600.9ms p(90)=427.24ms p(95)=509.27ms
running (00m16.6s), 0/3 VUs, 15 complete and 0 interrupted iterations
ui_testing ✓ [======================================] 3 VUs 00m16.6s/10m0s 15/15 iters, 5 per VU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment