Skip to content

Commit 503b256

Browse files
committed
Upgrade to .NET 10, Bootstrap 5.3.8, and add Playwright E2E tests
- Upgrade target framework to net10.0 and all NuGet packages to latest - Enable nullable reference types; add required modifiers to File model - Switch Bootstrap from local file to 5.3.8 CDN; fix all stale BS4 class names - Add CompressionLevel.SmallestSize compression option - Migrate Google Analytics from deprecated UA to GA4 (G-DTV3FZ5XWK) - Replace plain Loading text with Bootstrap spinner - Update GitHub Actions to checkout@v4 and static-web-apps-deploy@v1 - Remove IIS Express profile from launchSettings.json - Fix PWA theme_color to Bootstrap 5 primary (#0d6efd) - Add Playwright E2E test suite covering compress/decompress round-trips
1 parent c8fded4 commit 503b256

14 files changed

Lines changed: 246 additions & 70 deletions

.github/workflows/azure-static-web-apps-victorious-plant-0abf84a0f.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ jobs:
1515
runs-on: ubuntu-latest
1616
name: Build and Deploy Job
1717
steps:
18-
- uses: actions/checkout@v2
18+
- uses: actions/checkout@v4
1919
with:
2020
submodules: true
2121
- name: Build And Deploy
2222
id: builddeploy
23-
uses: Azure/static-web-apps-deploy@v0.0.1-preview
23+
uses: Azure/static-web-apps-deploy@v1
2424
with:
2525
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_PLANT_0ABF84A0F }}
2626
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
@@ -39,7 +39,7 @@ jobs:
3939
steps:
4040
- name: Close Pull Request
4141
id: closepullrequest
42-
uses: Azure/static-web-apps-deploy@v0.0.1-preview
42+
uses: Azure/static-web-apps-deploy@v1
4343
with:
4444
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_PLANT_0ABF84A0F }}
4545
action: "close"

App.razor

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
@using System.IO
1+
@using System.IO
22
@using System.IO.Compression
33
@inject BlazorDownloadFile.IBlazorDownloadFileService BlazorDownloadFileService
44

5-
<div class="form-group">
5+
<div class="mb-3">
66
<div class="form-check form-check-inline">
77
<input id="radio-compress" class="form-check-input" type="radio"
88
checked="@(compressionMode == CompressionMode.Compress)"
@@ -18,9 +18,15 @@
1818
</div>
1919
@if (compressionMode == CompressionMode.Compress)
2020
{
21-
<div class="form-group">
22-
<label>Compression Level:</label>
23-
<div class="form-group">
21+
<div class="mb-3">
22+
<label class="form-label">Compression Level:</label>
23+
<div>
24+
<div class="form-check form-check-inline">
25+
<input id="radio-smallest" class="form-check-input" type="radio"
26+
checked="@(compressionLevel == CompressionLevel.SmallestSize)"
27+
@onchange="@(() => compressionLevel = CompressionLevel.SmallestSize)">
28+
<label class="form-check-label" for="radio-smallest">Smallest Size</label>
29+
</div>
2430
<div class="form-check form-check-inline">
2531
<input id="radio-optimal" class="form-check-input" type="radio"
2632
checked="@(compressionLevel == CompressionLevel.Optimal)"
@@ -43,21 +49,19 @@
4349
</div>
4450
}
4551

46-
<div class="form-group">
47-
<div class="custom-file">
48-
<InputFile OnChange="OnFilesChange" multiple class="custom-file-input" />
49-
@if (compressionMode == CompressionMode.Compress)
50-
{
51-
<label class="custom-file-label" for="customFile">Choose files to compress to gzip</label>
52-
}
53-
else
54-
{
55-
<label class="custom-file-label" for="customFile">Choose gzip files to decompress</label>
56-
}
57-
</div>
52+
<div class="mb-3">
53+
@if (compressionMode == CompressionMode.Compress)
54+
{
55+
<label class="form-label">Choose files to compress to gzip</label>
56+
}
57+
else
58+
{
59+
<label class="form-label">Choose gzip files to decompress</label>
60+
}
61+
<InputFile OnChange="OnFilesChange" multiple class="form-control" />
5862
</div>
5963

60-
<div class="form-group">
64+
<div class="mb-3">
6165
@if (compressionMode == CompressionMode.Compress)
6266
{
6367
<button role="button" class="btn btn-primary" @onclick="CompressFiles" disabled="@(!anyFiles)">Compress Files</button>
@@ -80,7 +84,7 @@
8084
@if(file.Status == "Compressing" || file.Status == "Decompressing")
8185
{
8286
<div class="spinner-grow text-primary spinner-grow-sm" role="status">
83-
<span class="sr-only">@file.Status</span>
87+
<span class="visually-hidden">@file.Status</span>
8488
</div>
8589
}
8690
&nbsp;
@@ -119,12 +123,12 @@
119123
var tasks = new List<Task>();
120124
foreach (var file in files)
121125
{
122-
var task = Task.Run(async () =>
126+
var task = Task.Run(async () =>
123127
{
124128
file.Status = "Compressing";
125129
file.NewName = $"{file.OriginalName}.gz";
126130
StateHasChanged();
127-
131+
128132
try
129133
{
130134
var browserFile = file.BrowserFile;
@@ -158,7 +162,7 @@
158162
var tasks = new List<Task>();
159163
foreach (var file in files)
160164
{
161-
var task = Task.Run(async () =>
165+
var task = Task.Run(async () =>
162166
{
163167
file.Status = "Decompressing";
164168
file.NewName = file.OriginalName.Replace(".gz", "");
@@ -191,4 +195,4 @@
191195

192196
await Task.WhenAll(tasks);
193197
}
194-
}
198+
}

BlazorDeCompressor.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
67
<PublishTrimmed>true</PublishTrimmed>
78
<InvariantGlobalization>true</InvariantGlobalization>
8-
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BlazorDownloadFile" Version="2.1.2" />
13-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
14-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
12+
<PackageReference Include="BlazorDownloadFile" Version="2.4.0.2" />
13+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
14+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.2" PrivateAssets="all" />
1515
</ItemGroup>
1616

1717
<ItemGroup>

Models/File.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ namespace BlazorDeCompressor.Models
44
{
55
public class File
66
{
7-
public IBrowserFile BrowserFile { get; set; }
8-
public string OriginalName { get; set; }
9-
public string NewName { get; set; }
10-
public string Status { get; set; }
7+
public required IBrowserFile BrowserFile { get; set; }
8+
public required string OriginalName { get; set; }
9+
public string? NewName { get; set; }
10+
public required string Status { get; set; }
1111
}
1212
}

Properties/launchSettings.json

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
11
{
2-
"iisSettings": {
3-
"windowsAuthentication": false,
4-
"anonymousAuthentication": true,
5-
"iisExpress": {
6-
"applicationUrl": "http://localhost:57527",
7-
"sslPort": 44391
8-
}
9-
},
102
"profiles": {
11-
"IIS Express": {
12-
"commandName": "IISExpress",
13-
"launchBrowser": true,
14-
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
15-
"environmentVariables": {
16-
"ASPNETCORE_ENVIRONMENT": "Development"
17-
}
18-
},
193
"BlazorDeCompressor": {
204
"commandName": "Project",
21-
"dotnetRunMessages": "true",
5+
"dotnetRunMessages": true,
226
"launchBrowser": true,
237
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
248
"applicationUrl": "https://localhost:5001;http://localhost:5000",

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# BlazorDeCompressor
22

33
This tool can compress and decompress files using the GZIP algorithm right inside the browser without transmitting files over the network.
4-
Learn more about this tool at https://swimburger.net/blog/dotnet/introducing-online-gzip-decompressor.
5-
6-
You can use this tool as **[gzip.swimburger.net](https://gzip.swimburger.net)**.
4+
Learn more about this tool at https://swimburger.net/blog/dotnet/introducing-online-gzip-decompressor.
5+
6+
You can use this tool at **[gzip.swimburger.net](https://gzip.swimburger.net)**.
7+
8+
## Requirements
9+
10+
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
11+
12+
## Run locally
13+
14+
```bash
15+
dotnet run
16+
```

e2e/package-lock.json

Lines changed: 79 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "e2e",
3+
"scripts": {
4+
"test": "playwright test"
5+
},
6+
"devDependencies": {
7+
"@playwright/test": "^1.58.2"
8+
}
9+
}

e2e/playwright.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @ts-check
2+
const { defineConfig } = require('@playwright/test');
3+
4+
module.exports = defineConfig({
5+
testDir: './tests',
6+
use: {
7+
baseURL: 'https://localhost:5001',
8+
ignoreHTTPSErrors: true,
9+
},
10+
});

e2e/tests/app.spec.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// @ts-check
2+
const { test, expect } = require('@playwright/test');
3+
const path = require('path');
4+
const fs = require('fs');
5+
const os = require('os');
6+
7+
test.beforeEach(async ({ page }) => {
8+
await page.goto('/');
9+
// Wait for Blazor WASM to finish loading
10+
await page.waitForSelector('.spinner-border', { state: 'hidden', timeout: 30000 });
11+
});
12+
13+
test('page title and heading', async ({ page }) => {
14+
await expect(page).toHaveTitle(/GZIP/i);
15+
await expect(page.getByRole('heading', { name: /Online GZIP de\/compressor/i })).toBeVisible();
16+
});
17+
18+
test('compress mode is selected by default', async ({ page }) => {
19+
await expect(page.locator('#radio-compress')).toBeChecked();
20+
await expect(page.locator('#radio-decompress')).not.toBeChecked();
21+
});
22+
23+
test('all four compression levels are shown in compress mode', async ({ page }) => {
24+
await expect(page.locator('#radio-smallest')).toBeVisible();
25+
await expect(page.locator('#radio-optimal')).toBeVisible();
26+
await expect(page.locator('#radio-fastest')).toBeVisible();
27+
await expect(page.locator('#radio-no-compression')).toBeVisible();
28+
await expect(page.locator('#radio-optimal')).toBeChecked();
29+
});
30+
31+
test('compress button is disabled with no files selected', async ({ page }) => {
32+
await expect(page.getByRole('button', { name: 'Compress Files' })).toBeDisabled();
33+
await expect(page.getByText('No files selected')).toBeVisible();
34+
});
35+
36+
test('compression level options are hidden in decompress mode', async ({ page }) => {
37+
await page.locator('#radio-decompress').check();
38+
await expect(page.locator('#radio-optimal')).not.toBeVisible();
39+
await expect(page.getByRole('button', { name: 'Decompress Files' })).toBeDisabled();
40+
});
41+
42+
test('compress a file end-to-end', async ({ page }) => {
43+
// Create a temp file to compress
44+
const tmpFile = path.join(os.tmpdir(), 'test-input.txt');
45+
fs.writeFileSync(tmpFile, 'Hello, GZIP!');
46+
47+
const downloadPromise = page.waitForEvent('download');
48+
await page.locator('input[type="file"]').setInputFiles(tmpFile);
49+
await page.getByRole('button', { name: 'Compress Files' }).click();
50+
51+
const download = await downloadPromise;
52+
expect(download.suggestedFilename()).toBe('test-input.txt.gz');
53+
54+
// Verify the status shows Finished
55+
await expect(page.getByText('✔ Finished')).toBeVisible({ timeout: 10000 });
56+
57+
fs.unlinkSync(tmpFile);
58+
});
59+
60+
test('decompress a file end-to-end', async ({ page }) => {
61+
// Create a real gzip file to decompress
62+
const { execSync } = require('child_process');
63+
const tmpDir = os.tmpdir();
64+
const srcFile = path.join(tmpDir, 'test-decompress.txt');
65+
const gzFile = path.join(tmpDir, 'test-decompress.txt.gz');
66+
fs.writeFileSync(srcFile, 'Hello, GZIP decompressed!');
67+
execSync(`gzip -kf ${srcFile}`);
68+
69+
await page.locator('#radio-decompress').check();
70+
71+
const downloadPromise = page.waitForEvent('download');
72+
await page.locator('input[type="file"]').setInputFiles(gzFile);
73+
await page.getByRole('button', { name: 'Decompress Files' }).click();
74+
75+
const download = await downloadPromise;
76+
expect(download.suggestedFilename()).toBe('test-decompress.txt');
77+
78+
await expect(page.getByText('✔ Finished')).toBeVisible({ timeout: 10000 });
79+
80+
fs.unlinkSync(srcFile);
81+
fs.unlinkSync(gzFile);
82+
});

0 commit comments

Comments
 (0)