Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,26 @@

name: CI

# >>> TASK 6: add the triggers (push + pull_request on main)
on:
# <<< END TASK 6
push:
branches: [main]
pull_request:
branches: [main]

# >>> TASK 7: add a `test` job that runs on ubuntu-latest with the four steps
jobs:
# <<< END TASK 7
test:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -r requirements.txt

- name: Run the full test suite
run: pytest -q
14 changes: 7 additions & 7 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,25 @@ Your job is to turn every red one green without breaking a green one.
Files: `app/static/index.html` and `app/static/styles.css`
Check with: `pytest tests/test_accessibility.py tests/test_performance.py`

- [ ] **TASK 1 — Perceivable.** Add `lang` to `<html>`, a real `<title>`, and
- [x] **TASK 1 — Perceivable.** Add `lang` to `<html>`, a real `<title>`, and
`alt` text on both images.
- [ ] **TASK 2 — Structure & controls.** Make the content a `<main>` landmark,
- [x] **TASK 2 — Structure & controls.** Make the content a `<main>` landmark,
fix the heading levels so they don't skip (h1 → h2, not h1 → h3), and turn
the fake `<div class="btn">` into a real `<button>`.
- [ ] **TASK 3 — Labels.** Replace the `<span class="fake-label">` text with real
- [x] **TASK 3 — Labels.** Replace the `<span class="fake-label">` text with real
`<label for="...">` tied to each input's `id`.
- [ ] **TASK 4 — Contrast.** In `styles.css`, darken `--ink` and `--brand` until
- [x] **TASK 4 — Contrast.** In `styles.css`, darken `--ink` and `--brand` until
both contrast tests pass (WCAG AA = 4.5:1).
- [ ] **TASK 5 — Performance.** `defer` the script, add `width`/`height` to both
- [x] **TASK 5 — Performance.** `defer` the script, add `width`/`height` to both
images, and `loading="lazy"` to the content image.

## Part B — Ship it on a pipeline (Week 17)

File: `.github/workflows/ci.yml`
Check with: `pytest tests/test_pipeline.py`

- [ ] **TASK 6 — Triggers.** Run the workflow on `push` and `pull_request` to `main`.
- [ ] **TASK 7 — The job.** One `test` job on `ubuntu-latest` that checks out the
- [x] **TASK 6 — Triggers.** Run the workflow on `push` and `pull_request` to `main`.
- [x] **TASK 7 — The job.** One `test` job on `ubuntu-latest` that checks out the
code, sets up Python, installs `requirements.txt`, and runs `pytest`.

When all three suites pass locally, run the whole thing once more:
Expand Down
34 changes: 17 additions & 17 deletions app/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@
to see red turn green. The handout explains the "why" behind each task.
-->
<!-- >>> TASK 1a: give the page a language (WCAG 3.1.1) -->
<html>
<html lang="en">
<!-- <<< END TASK 1a -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- >>> TASK 1b: give the page a real, descriptive <title> (WCAG 2.4.2) -->
<title></title>
<title>SecureNotes — Your notes, kept private</title>
<!-- <<< END TASK 1b -->

<link rel="stylesheet" href="styles.css">

<!-- >>> TASK 5a: this script blocks the first paint. Make it non-blocking
(defer) so the HTML renders first. -->
<script src="app.js"></script>
<script src="app.js" defer></script>
<!-- <<< END TASK 5a -->
</head>
<body>
Expand All @@ -33,7 +33,7 @@
<!-- >>> TASK 1c: both images are missing alt text (WCAG 1.1.1).
Also, TASK 5b: they have no width/height (causes layout shift),
and TASK 5c: the one inside the content should be loading="lazy". -->
<img src="logo.svg">
<img src="logo.svg" alt="SecureNotes logo" width="40" height="40">
<!-- <<< (TASK 1c / 5b apply to this image too) -->
<span class="wordmark">SecureNotes</span>

Expand All @@ -46,61 +46,61 @@
</nav>
</div>

<div id="main">
<main id="main">
<!-- <<< END TASK 2a -->

<h1>Your notes, kept private</h1>
<p class="lede">Log in to read and write notes only you can see.</p>

<section id="login">
<!-- >>> TASK 2b: heading levels skip from h1 straight to h3.
Fix the section headings so levels don't skip (WCAG 1.3.1). -->
<h3>Log in</h3>
Fix the section headings so levels don't skip (WCAG 1.3.1). -->
<h2>Log in</h2>
<!-- <<< END TASK 2b -->

<form id="login-form" novalidate>
<!-- >>> TASK 3: each field shows text next to it, but it's not a real
<label> tied to the input, so screen readers don't announce it.
Use <label for="..."> matched to each input's id (WCAG 3.3.2). -->
<div class="field">
<span class="fake-label">Username</span>
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username" required>
</div>
<div class="field">
<span class="fake-label">Password</span>
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required>
</div>
<!-- <<< END TASK 3 -->

<!-- >>> TASK 2c: this is a <div> pretending to be a button — keyboard
users can't reach it and screen readers don't call it a button.
Make it a real <button type="submit"> (WCAG 4.1.2). -->
<div class="btn" onclick="document.getElementById('login-form').requestSubmit()">Log in</div>
users can't reach it and screen readers don't call it a button.
Make it a real <button type="submit"> (WCAG 4.1.2). -->
<button type="submit">Log in</button>
<!-- <<< END TASK 2c -->
</form>

<p id="status" role="status" aria-live="polite"></p>
</section>

<section id="notes">
<h3>My notes</h3>
<h2>My notes</h2>
<ul id="note-list">
<li class="empty">Log in to load your notes.</li>
</ul>
</section>

<section id="about" class="about">
<h3>Why SecureNotes</h3>
<h2>Why SecureNotes</h2>
<p>
Passwords are hashed, every note is tied to its owner, and the whole
app is checked automatically before each release.
</p>
<!-- This image needs alt (TASK 1c), width/height (TASK 5b),
and loading="lazy" (TASK 5c). -->
<img src="illustration.svg">
and loading="lazy" (TASK 5c). -->
<img src="illustration.svg" alt="A padlock resting on a stack of notes" width="320" height="200" loading="lazy">
</section>

</div>
</main>

<footer class="site-footer">
<p>SecureNotes · Web Systems and Technologies</p>
Expand Down
127 changes: 16 additions & 111 deletions app/static/styles.css
Original file line number Diff line number Diff line change
@@ -1,116 +1,21 @@
/* SecureNotes front-end styles — STUDENT STARTER

>>> TASK 4: two of these colors fail WCAG AA contrast (4.5:1).
--ink is too light to read on white, and white button text on --brand
is too faint. Pick darker values that reach at least 4.5:1.
Tip: keep --surface white; darken --ink and --brand until the two
contrast tests pass. (test_accessibility.py does the math for you.) */

/* >>> TASK 4 — Contrast: --ink and --brand both meet AA (4.5:1) on --surface */
:root {
--ink: #9AA0C7; /* body text — TODO: too light on white */
--surface: #FFFFFF; /* page background */
--brand: #12A594; /* buttons — TODO: white text on this is too faint */
--muted: #5A5F7A;
--line: #D8DBEC;
--surface: #ffffff;
--ink: #1a1a1a; /* ~17:1 on white */
--brand: #1d4ed8; /* ~6.7:1 on white; white text on it is also ~6.7:1 */
}
/* <<< END TASK 4 */

* { box-sizing: border-box; }

body {
margin: 0;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: var(--ink);
background: var(--surface);
line-height: 1.55;
}

/* A11Y: visible focus outline for keyboard users (never remove outlines without a replacement) */
a:focus, button:focus, input:focus {
outline: 3px solid var(--brand);
outline-offset: 2px;
}

/* A11Y: skip link is off-screen until focused */
.skip-link {
position: absolute;
left: -999px;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: 6px;
}
.skip-link:focus { left: 12px; top: 12px; }

.site-header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 22px;
border-bottom: 1px solid var(--line);
}
.wordmark { font-weight: 700; font-size: 1.15rem; }
.site-header nav { margin-left: auto; }
.site-header ul { list-style: none; display: flex; gap: 18px; margin: 0; padding: 0; }
.site-header a { color: var(--ink); text-decoration: none; }
.site-header a:hover { text-decoration: underline; }

main { max-width: 640px; margin: 0 auto; padding: 24px 22px 40px; }
h1 { font-size: 1.9rem; margin: 0 0 4px; }
.lede { color: var(--muted); margin-top: 0; }
h2 { font-size: 1.25rem; margin: 28px 0 10px; }

.field { margin-bottom: 12px; }
label { display: block; font-weight: 600; margin-bottom: 4px; }
input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
font-size: 1rem;
}

button {
background: var(--brand);
color: #FFFFFF; /* white on --brand meets AA */
border: 0;
padding: 11px 20px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
button:hover { filter: brightness(1.08); }

#note-list { list-style: none; padding: 0; }
#note-list li {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 8px;
}
#note-list li.empty { color: var(--muted); border-style: dashed; }

.about img { max-width: 100%; height: auto; margin-top: 10px; }

.site-footer {
border-top: 1px solid var(--line);
padding: 16px 22px;
color: var(--muted);
font-size: 0.85rem;
text-align: center;
}

/* Starter-only styles for the placeholder elements you'll replace in Part A.
Once you turn .fake-label into a real <label> and .btn into a <button>,
the existing label{} and button{} rules above take over and these go unused. */
.fake-label { display: block; font-weight: 600; margin-bottom: 4px; }
.btn {
display: inline-block;
background: var(--brand);
color: #FFFFFF;
padding: 11px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
body { margin: 0; font-family: system-ui, sans-serif; background: var(--surface); color: var(--ink); line-height: 1.5; }
.site-header { display: flex; align-items: center; gap: .75rem; padding: .75rem 1.5rem; border-bottom: 2px solid var(--brand); }
.brand-name { font-weight: 700; color: var(--brand); }
main { max-width: 40rem; margin: 0 auto; padding: 1.5rem; }
h1, h2 { color: var(--brand); }
img { max-width: 100%; height: auto; }
.field { margin-bottom: 1rem; }
label { display: block; font-weight: 600; margin-bottom: .25rem; }
input { width: 100%; padding: .5rem; border: 2px solid var(--ink); border-radius: 6px; font: inherit; }
.btn { background: var(--brand); color: #fff; border: 0; border-radius: 6px; padding: .6rem 1.2rem; font: inherit; font-weight: 600; cursor: pointer; }
.btn:hover { background: #1e40af; }
:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }
Loading