๐ Teaching Flow
1. Story Intro (5 min)
๐ "Imagine your tests are like a restaurant kitchen - they work hard behind the scenes. Allure Reports is like the fancy menu that shows customers (stakeholders) how everything works beautifully. ExcelJS is like the inventory system - it helps you organize and track all your test data in spreadsheets."
Real-World Analogy:
Allure Reports = Beautiful restaurant menu with photos, descriptions, and ratings
ExcelJS = Digital filing cabinet that can read, write, and organize Excel files automatically
Allure Reports = Beautiful restaurant menu with photos, descriptions, and ratings
ExcelJS = Digital filing cabinet that can read, write, and organize Excel files automatically
Part 1: Allure Reports (60 min)
What is Allure Reports?
Allure Reports creates beautiful, interactive HTML reports from your test results. Instead of plain text logs, you get fancy charts, graphs, and detailed test information that stakeholders love to see.
Why Use Allure Reports?
Benefits:
- Beautiful Reports: Professional-looking HTML reports with charts
- Stakeholder Friendly: Easy for non-technical people to understand
- Detailed Analytics: Test trends, flaky tests, and performance metrics
- CI/CD Integration: Perfect for automated pipelines
- Historical Data: Compare test runs over time
Installation & Setup
Step 1: Install Allure
# Install Allure Command Line Tool npm install -g allure-commandline # Or install locally in your project npm install --save-dev allure-commandline
Step 2: Install Playwright Allure Reporter
# Install the Allure reporter for Playwright npm install --save-dev allure-playwright
Step 3: Configure playwright.config.js
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
reporter: [
['html'], // Keep HTML reporter
['allure-playwright', {
detail: true,
outputFolder: 'allure-results',
suiteTitle: false
}]
],
use: {
baseURL: 'https://learn-playwright.great-site.net',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
});
Generating Allure Reports
Step 1: Run Tests with Allure
# Run your tests (Allure data will be generated automatically) npx playwright test # Generate and open Allure report npx allure generate allure-results --clean npx allure open
Enhanced Test Annotations
๐ tests/enhanced-test.spec.js
import { test, expect } from '@playwright/test';
import { allure } from 'allure-playwright';
test.describe('Enhanced E-commerce Tests', () => {
test('User can add product to cart', async ({ page }) => {
// Add test metadata for Allure
await allure.epic('E-commerce');
await allure.feature('Shopping Cart');
await allure.story('Add Product to Cart');
await allure.severity('critical');
await allure.owner('QA Team');
await page.goto('https://learn-playwright.great-site.net');
await allure.step('Navigate to homepage', async () => {
await page.getByRole('link', { name: 'Clothes' }).click();
});
await allure.step('Add product to cart', async () => {
await page.getByRole('button', { name: 'Add to cart' }).first().click();
});
await allure.step('Verify cart update', async () => {
await expect(page.getByText('Product successfully added')).toBeVisible();
});
});
});
Part 2: ExcelJS - Excel File Handling (60 min)
What is ExcelJS? (Simple Explanation)
ExcelJS is like a magic translator between your computer code and Excel spreadsheets.
Think of it this way:
Example: Instead of manually opening Excel and typing data, you tell ExcelJS: "Hey robot, go to row 3, column 2, and write 'Apple' there!"
Think of it this way:
- ๐ Excel File = A book with tables
- ๐ค ExcelJS = A robot that can read and write in that book
- ๐ป Your Code = Instructions you give to the robot
Example: Instead of manually opening Excel and typing data, you tell ExcelJS: "Hey robot, go to row 3, column 2, and write 'Apple' there!"
Why Use ExcelJS Instead of Manual Excel?
| Manual Excel Work | ExcelJS (Robot Work) |
|---|---|
| ๐ค You click and type manually | ๐ค Code does it automatically |
| โฐ Takes 5 minutes per file | โก Takes 1 second per file |
| ๐ด You get tired after 10 files | ๐ Robot never gets tired |
| โ You might make typos | โ Robot follows exact instructions |
Step 1: Installation (Like Installing an App)
Install ExcelJS (One Command)
npm install exceljs
What this does: Downloads the ExcelJS "robot" to your project so you can use it.
Step 2: Create a Sample Excel File (Our Practice Book)
Let's Create: fruits-data.xlsx
๐ Sample Excel File Structure: | A (Name) | B (Color) | C (Price) | D (Stock) | E (Category) | |-------------|--------------|-----------|-----------|--------------| | Apple | Red | $1.50 | 100 | Fruit | | Banana | Yellow | $0.80 | 150 | Fruit | | Orange | Orange | $1.20 | 80 | Fruit | | Carrot | Orange | $0.60 | 200 | Vegetable | | Broccoli | Green | $1.80 | 50 | Vegetable |
Step 3: Basic ExcelJS Setup (Getting the Robot Ready)
๐ basic-excel-example.js
// Step 1: Import ExcelJS (Get the robot)
import ExcelJS from 'exceljs';
// Step 2: Create a new workbook (Get a new book)
const workbook = new ExcelJS.Workbook();
console.log('๐ค ExcelJS robot is ready to work!');
Step 4: Opening an Excel File (Robot Opens the Book)
Opening a File - Step by Step
// Step 1: Tell the robot which file to open
const filePath = './fruits-data.xlsx';
// Step 2: Robot opens the file
await workbook.xlsx.readFile(filePath);
// Step 3: Get the first worksheet (first page of the book)
const worksheet = workbook.getWorksheet(1); // or workbook.getWorksheet('Sheet1')
console.log('๐ File opened successfully!');
console.log('๐ Worksheet name:', worksheet.name);
console.log('๐ Total rows:', worksheet.rowCount);
console.log('๐ Total columns:', worksheet.columnCount);
Step 5: Reading a Single Cell (Robot Reads One Word)
Reading One Cell at a Time
// Method 1: Read by row and column numbers
const cellA1 = worksheet.getCell('A1').value; // Row 1, Column A
const cellB2 = worksheet.getCell('B2').value; // Row 2, Column B
console.log('Cell A1 (Header):', cellA1); // "Name"
console.log('Cell B2 (First fruit name):', cellB2); // "Apple"
// Method 2: Read by row and column numbers
const cellRow1Col1 = worksheet.getRow(1).getCell(1).value;
const cellRow2Col2 = worksheet.getRow(2).getCell(2).value;
console.log('Row 1, Col 1:', cellRow1Col1);
console.log('Row 2, Col 2:', cellRow2Col2);
Step 6: Reading an Entire Column (Robot Reads One Column)
Reading All Data in Column A (Names)
// Read all values in Column A (Names column)
const namesColumn = [];
// Start from row 2 (skip header row 1)
for (let row = 2; row <= worksheet.rowCount; row++) {
const cellValue = worksheet.getCell(`A${row}`).value;
if (cellValue) { // Only add if cell has data
namesColumn.push(cellValue);
}
}
console.log('๐ All names in Column A:');
namesColumn.forEach((name, index) => {
console.log(`${index + 1}. ${name}`);
});
// Output:
// 1. Apple
// 2. Banana
// 3. Orange
// 4. Carrot
// 5. Broccoli
Step 7: Reading All Data with a Loop (Robot Reads Everything)
Reading All Rows and Columns
// Read all data from the Excel file
const allData = [];
// Loop through all rows (skip header row 1)
for (let row = 2; row <= worksheet.rowCount; row++) {
const rowData = {
name: worksheet.getCell(`A${row}`).value,
color: worksheet.getCell(`B${row}`).value,
price: worksheet.getCell(`C${row}`).value,
stock: worksheet.getCell(`D${row}`).value,
category: worksheet.getCell(`E${row}`).value
};
allData.push(rowData);
}
console.log('๐ All data from Excel:');
allData.forEach((item, index) => {
console.log(`${index + 1}. ${item.name} - ${item.color} - $${item.price} - Stock: ${item.stock} - ${item.category}`);
});
// Output:
// 1. Apple - Red - $1.50 - Stock: 100 - Fruit
// 2. Banana - Yellow - $0.80 - Stock: 150 - Fruit
// 3. Orange - Orange - $1.20 - Stock: 80 - Fruit
// 4. Carrot - Orange - $0.60 - Stock: 200 - Vegetable
// 5. Broccoli - Green - $1.80 - Stock: 50 - Vegetable
Step 8: Finding Specific Rows with Conditions (Robot Searches)
Finding Rows Based on Conditions
// Find all fruits (Category = "Fruit")
const fruits = allData.filter(item => item.category === 'Fruit');
console.log('๐ All fruits:');
fruits.forEach(fruit => {
console.log(`- ${fruit.name} (${fruit.color})`);
});
// Find items with stock less than 100
const lowStock = allData.filter(item => item.stock < 100);
console.log('โ ๏ธ Low stock items:');
lowStock.forEach(item => {
console.log(`- ${item.name}: Only ${item.stock} left`);
});
// Find the most expensive item
const mostExpensive = allData.reduce((max, item) => {
const itemPrice = parseFloat(item.price.replace('$', ''));
const maxPrice = parseFloat(max.price.replace('$', ''));
return itemPrice > maxPrice ? item : max;
});
console.log('๐ฐ Most expensive item:', mostExpensive.name, `($${mostExpensive.price})`);
// Find items by color
const orangeItems = allData.filter(item => item.color === 'Orange');
console.log('๐งก Orange items:');
orangeItems.forEach(item => {
console.log(`- ${item.name} (${item.category})`);
});
Step 9: Writing/Changing Data in Specific Cells (Robot Writes)
Updating Specific Cells
// Method 1: Update a specific cell by address
worksheet.getCell('D2').value = 120; // Change Apple's stock to 120
worksheet.getCell('C3').value = '$0.90'; // Change Banana's price to $0.90
// Method 2: Update by row and column numbers
worksheet.getRow(4).getCell(4).value = 180; // Change Carrot's stock to 180
// Method 3: Update multiple cells in a row
const row5 = worksheet.getRow(5);
row5.getCell(3).value = '$2.00'; // Change Broccoli's price
row5.getCell(4).value = 75; // Change Broccoli's stock
console.log('โ๏ธ Updated specific cells!');
// Save the changes back to the file
await workbook.xlsx.writeFile('./fruits-data-updated.xlsx');
console.log('๐พ Changes saved to new file: fruits-data-updated.xlsx');
Step 10: Adding New Rows (Robot Adds New Lines)
Adding New Data to Excel
// Add a new row with new fruit data
const newRow = worksheet.addRow({
A: 'Grape', // Name
B: 'Purple', // Color
C: '$2.50', // Price
D: 60, // Stock
E: 'Fruit' // Category
});
console.log('โ Added new row for Grape');
// Add another new row
worksheet.addRow(['Strawberry', 'Red', '$3.00', 40, 'Fruit']);
console.log('โ Added new row for Strawberry');
// Save the updated file
await workbook.xlsx.writeFile('./fruits-data-with-new-items.xlsx');
console.log('๐พ File saved with new items!');
Step 11: Creating a Complete Excel File from Scratch
Creating a New Excel File
// Create a completely new workbook
const newWorkbook = new ExcelJS.Workbook();
// Add a new worksheet
const newWorksheet = newWorkbook.addWorksheet('Mobile Phones');
// Add headers (Row 1)
newWorksheet.getRow(1).values = ['Brand', 'Model', 'Price', 'Storage', 'Color'];
newWorksheet.getRow(1).font = { bold: true }; // Make headers bold
// Add data rows
const phoneData = [
['iPhone', '14 Pro', '$999', '128GB', 'Space Black'],
['Samsung', 'Galaxy S23', '$799', '256GB', 'Phantom Black'],
['Google', 'Pixel 7', '$599', '128GB', 'Obsidian'],
['OnePlus', '11', '$699', '256GB', 'Titan Black'],
['Xiaomi', '13 Pro', '$899', '512GB', 'Ceramic White']
];
// Add each row of data
phoneData.forEach((phone, index) => {
const row = newWorksheet.addRow(phone);
console.log(`๐ฑ Added phone: ${phone[0]} ${phone[1]}`);
});
// Save the new file
await newWorkbook.xlsx.writeFile('./mobile-phones.xlsx');
console.log('โ
Created new Excel file: mobile-phones.xlsx');
Step 12: Now Let's Create Our Utility File (The Smart Robot)
๐ utils/ExcelUtils.js - Our Smart Excel Robot
import ExcelJS from 'exceljs';
export class ExcelUtils {
constructor() {
this.workbook = new ExcelJS.Workbook();
console.log('๐ค ExcelUtils robot initialized!');
}
// Method 1: Read all data from an Excel file
async readExcelFile(filePath, sheetName = 1) {
try {
console.log(`๐ Opening file: ${filePath}`);
await this.workbook.xlsx.readFile(filePath);
const worksheet = this.workbook.getWorksheet(sheetName);
const allData = [];
// Read all rows (skip header)
for (let row = 2; row <= worksheet.rowCount; row++) {
const rowData = {};
// Read all columns in this row
for (let col = 1; col <= worksheet.columnCount; col++) {
const cellValue = worksheet.getRow(row).getCell(col).value;
const headerValue = worksheet.getRow(1).getCell(col).value;
rowData[headerValue] = cellValue;
}
allData.push(rowData);
}
console.log(`โ
Read ${allData.length} rows from Excel`);
return allData;
} catch (error) {
console.error('โ Error reading Excel file:', error);
throw error;
}
}
// Method 2: Write data to a new Excel file
async writeExcelFile(filePath, data, headers) {
try {
const worksheet = this.workbook.addWorksheet('Data');
// Add headers
worksheet.getRow(1).values = headers;
worksheet.getRow(1).font = { bold: true };
// Add data rows
data.forEach((rowData, index) => {
const row = worksheet.addRow(rowData);
console.log(`๐ Added row ${index + 1}: ${JSON.stringify(rowData)}`);
});
// Save file
await this.workbook.xlsx.writeFile(filePath);
console.log(`โ
Excel file created: ${filePath}`);
} catch (error) {
console.error('โ Error writing Excel file:', error);
throw error;
}
}
// Method 3: Update specific cell
async updateCell(filePath, row, column, newValue) {
try {
await this.workbook.xlsx.readFile(filePath);
const worksheet = this.workbook.getWorksheet(1);
worksheet.getRow(row).getCell(column).value = newValue;
await this.workbook.xlsx.writeFile(filePath);
console.log(`โ๏ธ Updated cell (${row}, ${column}) to: ${newValue}`);
} catch (error) {
console.error('โ Error updating cell:', error);
throw error;
}
}
// Method 4: Find rows with specific conditions
async findRows(filePath, condition) {
try {
const data = await this.readExcelFile(filePath);
const filteredData = data.filter(condition);
console.log(`๐ Found ${filteredData.length} rows matching condition`);
return filteredData;
} catch (error) {
console.error('โ Error finding rows:', error);
throw error;
}
}
}
Step 13: Using Our ExcelUtils Robot
๐ examples/using-excel-utils.js
import { ExcelUtils } from '../utils/ExcelUtils.js';
// Create our Excel robot
const excelRobot = new ExcelUtils();
async function demonstrateExcelUtils() {
try {
// Example 1: Read all data from fruits file
console.log('๐ Reading fruits data...');
const fruitsData = await excelRobot.readExcelFile('./fruits-data.xlsx');
console.log('Fruits data:', fruitsData);
// Example 2: Find all fruits (category = 'Fruit')
console.log('๐ Finding all fruits...');
const fruits = await excelRobot.findRows('./fruits-data.xlsx',
item => item.Category === 'Fruit'
);
console.log('Found fruits:', fruits);
// Example 3: Find items with low stock
console.log('โ ๏ธ Finding low stock items...');
const lowStock = await excelRobot.findRows('./fruits-data.xlsx',
item => item.Stock < 100
);
console.log('Low stock items:', lowStock);
// Example 4: Create a new Excel file with test results
console.log('๐ Creating test results file...');
const testResults = [
['Login Test', 'PASSED', '1200ms', 'No errors'],
['Search Test', 'FAILED', '800ms', 'Element not found'],
['Cart Test', 'PASSED', '1500ms', 'No errors']
];
await excelRobot.writeExcelFile('./test-results.xlsx', testResults,
['Test Name', 'Status', 'Duration', 'Error Message']
);
// Example 5: Update a specific cell
console.log('โ๏ธ Updating stock for Apple...');
await excelRobot.updateCell('./fruits-data.xlsx', 2, 4, 150); // Row 2, Column 4 (Stock)
} catch (error) {
console.error('โ Something went wrong:', error);
}
}
// Run our demonstration
demonstrateExcelUtils();
Data-Driven Testing with Excel
๐ tests/excel-data-driven.spec.js
import { test, expect } from '@playwright/test';
import { ExcelUtils } from '../utils/ExcelUtils';
test.describe('Data-Driven Login Tests', () => {
const excelUtils = new ExcelUtils();
let testData;
// Load test data from Excel before running tests
test.beforeAll(async () => {
testData = await excelUtils.readTestData('./test-data/login-test-data.xlsx');
});
test('Login with different user credentials', async ({ page }) => {
for (const data of testData) {
await test.step(`Login test: ${data.testCase}`, async () => {
await page.goto('https://learn-playwright.great-site.net');
await page.getByRole('link', { name: 'Sign in' }).click();
await page.fill('#email', data.username);
await page.fill('#password', data.password);
await page.getByRole('button', { name: 'Sign in' }).click();
if (data.expectedResult === 'Success') {
await expect(page.getByText('Welcome')).toBeVisible();
} else {
await expect(page.getByText('Invalid')).toBeVisible();
}
});
}
});
});
Part 3: File Upload & Download (30 min)
File Upload Testing
๐ tests/file-upload.spec.js
import { test, expect } from '@playwright/test';
import path from 'path';
test.describe('File Upload Tests', () => {
test('Upload Excel file', async ({ page }) => {
await page.goto('https://learn-playwright.great-site.net');
// Navigate to upload page (assuming there's an upload feature)
await page.getByRole('link', { name: 'Upload Data' }).click();
// Create a test Excel file
const testFilePath = path.join(process.cwd(), 'test-files', 'upload-test.xlsx');
// Upload the file
await page.setInputFiles('input[type="file"]', testFilePath);
// Submit the form
await page.getByRole('button', { name: 'Upload' }).click();
// Verify upload success
await expect(page.getByText('File uploaded successfully')).toBeVisible();
});
test('Upload multiple files', async ({ page }) => {
const files = [
path.join(process.cwd(), 'test-files', 'file1.xlsx'),
path.join(process.cwd(), 'test-files', 'file2.xlsx'),
path.join(process.cwd(), 'test-files', 'file3.xlsx')
];
await page.goto('https://learn-playwright.great-site.net/upload');
await page.setInputFiles('input[type="file"]', files);
await page.getByRole('button', { name: 'Upload All' }).click();
await expect(page.getByText('3 files uploaded successfully')).toBeVisible();
});
});
File Download & Wait for Download
๐ tests/file-download.spec.js
import { test, expect } from '@playwright/test';
import path from 'path';
import fs from 'fs';
test.describe('File Download Tests', () => {
test('Download Excel report and verify', async ({ page }) => {
// Start download before clicking
const downloadPromise = page.waitForEvent('download');
await page.goto('https://learn-playwright.great-site.net');
await page.getByRole('button', { name: 'Download Report' }).click();
// Wait for download to complete
const download = await downloadPromise;
// Specify download path
const downloadPath = path.join(process.cwd(), 'downloads', download.suggestedFilename());
// Save the downloaded file
await download.saveAs(downloadPath);
// Verify file exists and has content
expect(fs.existsSync(downloadPath)).toBeTruthy();
// Verify file size (not empty)
const stats = fs.statSync(downloadPath);
expect(stats.size).toBeGreaterThan(0);
console.log(`โ
File downloaded: ${downloadPath}`);
});
test('Download and read Excel content', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.goto('https://learn-playwright.great-site.net');
await page.getByRole('button', { name: 'Download Data' }).click();
const download = await downloadPromise;
const downloadPath = path.join(process.cwd(), 'downloads', download.suggestedFilename());
await download.saveAs(downloadPath);
// Read the downloaded Excel file
const { ExcelUtils } = await import('../utils/ExcelUtils.js');
const excelUtils = new ExcelUtils();
const downloadedData = await excelUtils.readTestData(downloadPath);
// Verify the downloaded data
expect(downloadedData.length).toBeGreaterThan(0);
expect(downloadedData[0]).toHaveProperty('testCase');
console.log('โ
Downloaded Excel file verified:', downloadedData.length, 'records');
});
});
Complete Excel Workflow
๐ tests/complete-excel-workflow.spec.js
import { test, expect } from '@playwright/test';
import { ExcelUtils } from '../utils/ExcelUtils';
test.describe('Complete Excel Workflow', () => {
const excelUtils = new ExcelUtils();
test('End-to-end Excel workflow', async ({ page }) => {
// Step 1: Read test data from Excel
const testData = await excelUtils.readTestData('./test-data/user-data.xlsx');
const results = [];
for (const user of testData) {
await test.step(`Test user: ${user.username}`, async () => {
await page.goto('https://learn-playwright.great-site.net');
await page.getByRole('link', { name: 'Sign in' }).click();
const startTime = Date.now();
await page.fill('#email', user.email);
await page.fill('#password', user.password);
await page.getByRole('button', { name: 'Sign in' }).click();
const duration = Date.now() - startTime;
let status = 'FAILED';
let error = '';
try {
if (user.expectedResult === 'Success') {
await expect(page.getByText('Welcome')).toBeVisible({ timeout: 5000 });
status = 'PASSED';
} else {
await expect(page.getByText('Invalid')).toBeVisible({ timeout: 5000 });
status = 'PASSED';
}
} catch (e) {
error = e.message;
}
results.push({
name: `Login Test - ${user.username}`,
status: status,
duration: duration,
error: error
});
});
}
// Step 2: Write results back to Excel
await excelUtils.createTestResultsFile('./test-results/test-execution-results.xlsx', results);
console.log(`โ
Test completed. Results written to Excel.`);
});
});
๐งโ๐ป Interactive Questions & Answers
1. Why use Allure Reports instead of basic HTML reports?
Answer:
- Visual Appeal: Allure creates beautiful, interactive reports with charts and graphs
- Stakeholder Friendly: Non-technical people can easily understand test results
- Historical Trends: Shows test performance over time with trends
- Detailed Analytics: Identifies flaky tests, performance issues, and bottlenecks
- CI/CD Integration: Perfect for automated pipelines and team collaboration
2. What's the difference between reading and writing Excel files?
Answer:
- Reading: Extract data from existing Excel files for test data
- Writing: Create new Excel files with test results and reports
- Reading Use Cases: Load test data, configuration, user credentials
- Writing Use Cases: Save test results, generate reports, export data
- Combined Workflow: Read test data โ Run tests โ Write results back to Excel
3. How do you wait for file downloads to complete in Playwright?
Answer:
- waitForEvent('download'): Wait for download event before clicking download button
- saveAs(): Specify exact location to save the downloaded file
- File Verification: Check file exists and has content after download
- Example:
const downloadPromise = page.waitForEvent('download'); await page.click('#download-button'); const download = await downloadPromise; await download.saveAs('./downloads/report.xlsx');
4. What are the benefits of data-driven testing with Excel?
Answer:
- Test Data Management: Organize test data in a familiar spreadsheet format
- Non-Technical Updates: Business users can update test data without code changes
- Scalability: Easily add more test scenarios by adding rows
- Data Visualization: See test data clearly in tables and charts
- Version Control: Track changes to test data over time
๐ฏ Day 12 Homework Tasks
๐ข Beginner
Task 1
Set up Allure Reports and generate a basic report for existing tests.
Task 2
Create an Excel file with test data using ExcelJS and read it in a test.
๐ก Intermediate
Task 3
Implement data-driven testing using Excel data and write results back to Excel.
Task 4
Test file upload functionality and verify the uploaded file content.
๐ด Advanced
Task 5
Create a complete Excel workflow: read data โ run tests โ generate Allure report โ save results to Excel.
Task 6
Implement file download testing with wait conditions and content verification using ExcelJS.
Recommended Test Websites
Best Websites for Allure Reports & ExcelJS Testing:
- File Upload Testing:
https://the-internet.herokuapp.com/upload - File Download Testing:
https://the-internet.herokuapp.com/download - Form Testing:
https://demoqa.com/automation-practice-form - Data Tables:
https://demo.guru99.com/test/web-table-element.php - Dynamic Data:
https://reqres.in(for API + Excel integration)
โ Outcomes
- Set up and generate beautiful Allure Reports for Playwright tests
- Master ExcelJS for reading and writing Excel files programmatically
- Implement data-driven testing using Excel as test data source
- Handle file upload and download operations with proper wait conditions
- Create comprehensive test workflows combining Allure Reports and Excel data
- Generate stakeholder-friendly reports with visual analytics
- Build reusable utilities for Excel file operations
- Integrate Excel data with CI/CD pipelines for automated reporting