305 lines
19 KiB
Python
305 lines
19 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from typing import Iterable
|
||
|
||
from docx import Document
|
||
from docx.enum.section import WD_SECTION
|
||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||
from docx.oxml import OxmlElement
|
||
from docx.oxml.ns import qn
|
||
from docx.shared import Inches, Pt, RGBColor
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
OUT = ROOT / "evidence-documents"
|
||
SHOTS = ROOT / "docs" / "evidence" / "screenshots"
|
||
|
||
INK = RGBColor(33, 32, 55)
|
||
BLUE = RGBColor(46, 116, 181)
|
||
DARK_BLUE = RGBColor(31, 77, 120)
|
||
MUTED = RGBColor(103, 101, 119)
|
||
PURPLE = RGBColor(98, 87, 217)
|
||
GREEN = RGBColor(11, 143, 116)
|
||
PALE = "F2F4F7"
|
||
|
||
|
||
def set_font(run, size: float, color=INK, bold: bool | None = None, italic: bool | None = None):
|
||
run.font.name = "Calibri"
|
||
run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), "Calibri")
|
||
run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), "Calibri")
|
||
run.font.size = Pt(size)
|
||
run.font.color.rgb = color
|
||
if bold is not None:
|
||
run.bold = bold
|
||
if italic is not None:
|
||
run.italic = italic
|
||
|
||
|
||
def shade_paragraph(paragraph, fill: str, border: str = "D9DCE4"):
|
||
p_pr = paragraph._p.get_or_add_pPr()
|
||
shd = OxmlElement("w:shd")
|
||
shd.set(qn("w:fill"), fill)
|
||
p_pr.append(shd)
|
||
borders = OxmlElement("w:pBdr")
|
||
for side in ("top", "left", "bottom", "right"):
|
||
edge = OxmlElement(f"w:{side}")
|
||
edge.set(qn("w:val"), "single")
|
||
edge.set(qn("w:sz"), "6")
|
||
edge.set(qn("w:color"), border)
|
||
edge.set(qn("w:space"), "6")
|
||
borders.append(edge)
|
||
p_pr.append(borders)
|
||
|
||
|
||
def add_page_number(paragraph):
|
||
paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||
run = paragraph.add_run("Learning Planner · Competency evidence · ")
|
||
set_font(run, 8.5, MUTED)
|
||
fld = OxmlElement("w:fldSimple")
|
||
fld.set(qn("w:instr"), "PAGE")
|
||
paragraph._p.append(fld)
|
||
|
||
|
||
def configure_document(doc: Document, title: str, competency: str):
|
||
section = doc.sections[0]
|
||
section.top_margin = Inches(0.82)
|
||
section.bottom_margin = Inches(0.78)
|
||
section.left_margin = Inches(1)
|
||
section.right_margin = Inches(1)
|
||
section.header_distance = Inches(0.42)
|
||
section.footer_distance = Inches(0.42)
|
||
|
||
normal = doc.styles["Normal"]
|
||
normal.font.name = "Calibri"
|
||
normal._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
|
||
normal._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
|
||
normal.font.size = Pt(11)
|
||
normal.font.color.rgb = INK
|
||
normal.paragraph_format.space_before = Pt(0)
|
||
normal.paragraph_format.space_after = Pt(6)
|
||
normal.paragraph_format.line_spacing = 1.1
|
||
|
||
for style_name, size, color, before, after in (
|
||
("Heading 1", 16, BLUE, 16, 8),
|
||
("Heading 2", 13, BLUE, 12, 6),
|
||
("Heading 3", 12, DARK_BLUE, 8, 4),
|
||
):
|
||
style = doc.styles[style_name]
|
||
style.font.name = "Calibri"
|
||
style._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
|
||
style._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
|
||
style.font.size = Pt(size)
|
||
style.font.bold = True
|
||
style.font.color.rgb = color
|
||
style.paragraph_format.space_before = Pt(before)
|
||
style.paragraph_format.space_after = Pt(after)
|
||
style.paragraph_format.keep_with_next = True
|
||
|
||
header = section.header.paragraphs[0]
|
||
header.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||
header_run = header.add_run(f"ENGINEERING COMPETENCY · {competency.upper()}")
|
||
set_font(header_run, 8.5, MUTED, bold=True)
|
||
add_page_number(section.footer.paragraphs[0])
|
||
|
||
doc.core_properties.title = title
|
||
doc.core_properties.subject = competency
|
||
doc.core_properties.author = "Learning Planner project"
|
||
|
||
|
||
def add_masthead(doc: Document, title: str, subtitle: str, status: str):
|
||
kicker = doc.add_paragraph()
|
||
kicker.paragraph_format.space_after = Pt(2)
|
||
run = kicker.add_run("COMPETENCY EVIDENCE BRIEF")
|
||
set_font(run, 9, PURPLE, bold=True)
|
||
|
||
p = doc.add_paragraph()
|
||
p.paragraph_format.space_after = Pt(4)
|
||
run = p.add_run(title)
|
||
set_font(run, 23, INK, bold=True)
|
||
|
||
p = doc.add_paragraph()
|
||
p.paragraph_format.space_after = Pt(12)
|
||
run = p.add_run(subtitle)
|
||
set_font(run, 11.5, MUTED)
|
||
|
||
meta = doc.add_paragraph()
|
||
meta.paragraph_format.space_before = Pt(0)
|
||
meta.paragraph_format.space_after = Pt(12)
|
||
meta.add_run("Project: ").bold = True
|
||
meta.add_run("Learning Planner ")
|
||
meta.add_run("Date: ").bold = True
|
||
meta.add_run("14 August 2026 ")
|
||
meta.add_run("Status: ").bold = True
|
||
status_run = meta.add_run(status)
|
||
status_run.font.color.rgb = GREEN if status == "Implemented" else RGBColor(151, 99, 12)
|
||
|
||
|
||
def add_section(doc: Document, heading: str, paragraphs: Iterable[tuple[str | None, str]]):
|
||
doc.add_heading(heading, level=2)
|
||
for label, text in paragraphs:
|
||
p = doc.add_paragraph()
|
||
if label:
|
||
lead = p.add_run(f"{label}: ")
|
||
lead.bold = True
|
||
lead.font.color.rgb = DARK_BLUE
|
||
p.add_run(text)
|
||
|
||
|
||
def add_callout(doc: Document, label: str, text: str, fill: str = PALE):
|
||
p = doc.add_paragraph()
|
||
p.paragraph_format.left_indent = Inches(0.12)
|
||
p.paragraph_format.right_indent = Inches(0.12)
|
||
p.paragraph_format.space_before = Pt(5)
|
||
p.paragraph_format.space_after = Pt(9)
|
||
shade_paragraph(p, fill)
|
||
run = p.add_run(f"{label} ")
|
||
set_font(run, 10.5, DARK_BLUE, bold=True)
|
||
run = p.add_run(text)
|
||
set_font(run, 10.5, INK)
|
||
|
||
|
||
def add_appendix_page(doc: Document, evidence_id: str, caption: str, image_name: str):
|
||
doc.add_page_break()
|
||
kicker = doc.add_paragraph()
|
||
kicker.paragraph_format.space_after = Pt(2)
|
||
run = kicker.add_run("SCREENSHOT APPENDIX · EXCLUDED FROM TWO-PAGE NARRATIVE LIMIT")
|
||
set_font(run, 8.5, PURPLE, bold=True)
|
||
title = doc.add_paragraph()
|
||
title.paragraph_format.space_after = Pt(5)
|
||
run = title.add_run(f"{evidence_id} {caption}")
|
||
set_font(run, 15, INK, bold=True)
|
||
image = SHOTS / image_name
|
||
if image.exists():
|
||
p = doc.add_paragraph()
|
||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
p.paragraph_format.space_after = Pt(6)
|
||
shape = p.add_run().add_picture(str(image), width=Inches(6.25))
|
||
shape._inline.docPr.set("descr", caption)
|
||
shape._inline.docPr.set("title", evidence_id)
|
||
note = doc.add_paragraph()
|
||
note.paragraph_format.space_before = Pt(0)
|
||
note.paragraph_format.space_after = Pt(0)
|
||
note.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
run = note.add_run(f"Figure {evidence_id}. {caption}. Generated from non-sensitive sample data.")
|
||
set_font(run, 9, MUTED, italic=True)
|
||
|
||
|
||
def build_project_management():
|
||
doc = Document()
|
||
configure_document(doc, "Managing a Personal Engineering Project", "Create and manage personal projects")
|
||
add_masthead(doc, "Managing a Personal Engineering Project", "Planning, delivering and evidencing the Learning Planner web application.", "Implemented")
|
||
add_section(doc, "Objective", [(None, "Plan and deliver a realistic solo web application while making scope, dependencies, checkpoints, decisions and progress auditable to an engineering assessor.")])
|
||
add_section(doc, "Approach", [
|
||
("Scope", "I selected a local-first learning planner: substantial enough for typed state, CRUD, validation, responsive design and test automation, but bounded by excluding accounts, cloud sync and a backend."),
|
||
("Control", "I converted the work into P01–P14, linked dependencies, identified the critical path and defined six milestone exit checks. A versioned board snapshot, roadmap, test log and decision log preserve the evidence."),
|
||
])
|
||
add_section(doc, "Work completed", [
|
||
("Foundation", "Created the React/TypeScript/Vite project, quality scripts, stable domain interfaces, a versioned storage adapter and realistic sample data."),
|
||
("Product", "Delivered goal/task CRUD, dashboard summaries, search/filter/sort, validation, confirmation, JSON backup/recovery and accessible responsive interactions."),
|
||
("Governance", "Recorded acceptance criteria, dependency mapping, milestone status, risks and the RWD-01 decision. Manual Edge/device items remain visibly pending rather than being reported as complete."),
|
||
])
|
||
add_callout(doc, "Critical path", "P01 → P02/P03 → P04 → P05 → P06 → P07 → P10 → P11 → P12 → P13 → P14. Search/filter and backup proceeded as parallel branches before accessibility and final testing.")
|
||
|
||
doc.add_page_break()
|
||
add_section(doc, "Testing performed", [
|
||
("Quality gate", "Lint, nine unit/component tests, TypeScript compilation and the production build passed."),
|
||
("Browser workflows", "Playwright verified persisted goal creation, task search/status changes and horizontal-overflow protection across Google Chrome, Chromium, Firefox and all required automated viewports."),
|
||
("Checkpoint review", "I compared implementation against each backlog acceptance criterion and retained named test/evidence IDs. This exposed one visual defect at 1680×1050, which was fixed and retested."),
|
||
])
|
||
add_section(doc, "Outcome", [(None, "The project is runnable from a clean clone, the automated delivery scope is complete and the repository contains a portable audit trail. The assessor-ready release remains gated by genuine Edge and two-physical-device sessions, for which a signed checklist and screenshot naming protocol are supplied.")])
|
||
add_section(doc, "Lessons learned", [
|
||
("Make dependencies explicit", "The data model and responsive shell could progress in parallel, but CRUD depended on both; documenting that convergence kept sequencing clear."),
|
||
("Treat evidence as a deliverable", "Test IDs, seeded data and screenshot names must be designed early. Retrofitting them would have produced weaker, less repeatable evidence."),
|
||
("Report status truthfully", "A viewport emulator is not a physical device and Chromium is not Edge. Visible pending checkpoints are stronger engineering evidence than unsupported completion claims."),
|
||
])
|
||
add_callout(doc, "Evidence map", "PM-01 overview and acceptance criteria · PM-02 backlog/dependencies/roadmap · PM-03 test and checkpoint logs · Appendix figures show the delivered application and validated goal workflow.", "EEF0FF")
|
||
add_appendix_page(doc, "PM-04", "Evidence-ready application at 1920×1080", "responsive-1920x1080-dashboard.png")
|
||
add_appendix_page(doc, "PM-05", "Goal creation workflow at the 1680×1050 checkpoint", "responsive-1680x1050-goal-dialog.png")
|
||
return doc
|
||
|
||
|
||
def build_devices():
|
||
doc = Document()
|
||
configure_document(doc, "Testing Across Multiple Devices", "Test against multiple devices")
|
||
add_masthead(doc, "Testing Across Multiple Devices", "Responsive verification at three desktop resolutions with a reproducible physical-device protocol.", "Manual completion pending")
|
||
add_section(doc, "Objective", [(None, "Demonstrate that Learning Planner remains readable, operable and visually coherent at 1920×1080, 1680×1050 and 1920×1200, then repeat the principal workflow on two genuine computers.")])
|
||
add_section(doc, "Approach", [
|
||
("Repeatability", "Playwright set exact viewport dimensions and used the same non-sensitive dataset and core journey at every size. Each capture includes environment metadata."),
|
||
("Physical proof", "The manual protocol records manufacturer/model, OS, native resolution, scaling, browser version, inner viewport, tester and date for two computers. These fields cannot be completed by emulation."),
|
||
])
|
||
add_section(doc, "Work completed", [
|
||
("Responsive system", "Used a fixed desktop navigation rail, max-width content, Grid/Flexbox, wrapping controls, viewport-safe dialogs, visible focus, long-text protection and reduced-motion support."),
|
||
("Breakpoint intent", "At 1920 px the summary uses one four-card row; at 1680 px it becomes a balanced 2×2 grid; additional 1200 px height exposes more content without stretching controls."),
|
||
("Robustness", "A 768×900 check verifies the top-navigation and single-column fallback even though it is outside the formal evidence matrix."),
|
||
])
|
||
add_callout(doc, "Responsive elements", "Navigation, summary cards, progress and task panels, goal cards, filters, dates, long titles, validation, focus, dialogs, empty/error states and scrolling were included in the matrix.")
|
||
|
||
doc.add_page_break()
|
||
add_section(doc, "Testing performed", [
|
||
("Automated", "At all three required viewports the persisted CRUD journey, task interaction and page-level overflow assertion passed. The narrow robustness project also passed."),
|
||
("Visual", "Full-page captures were inspected for clipping, overlap, font wrapping, spacing, card balance and modal fit. `scrollWidth` equalled `clientWidth` at 1680 after the fix."),
|
||
("Zoom and keyboard", "The checklist covers 100%/125% zoom, Tab/Shift+Tab/Enter/Escape, focus visibility, long content and a clean application console."),
|
||
])
|
||
add_section(doc, "Outcome", [(None, "Automated responsive evidence passes for 1920×1080, 1680×1050 and 1920×1200. The two physical sessions are deliberately marked pending; this document is a working competency draft until their metadata and captures replace that status.")])
|
||
add_section(doc, "Lessons learned", [
|
||
("Functional pass is not visual quality", "At 1680×1050 the original three-column summary left one orphaned card. Automated overflow checks passed, but visual review exposed weak hierarchy."),
|
||
("Breakpoints should protect composition", "Changing to a 2×2 arrangement below 1800 px created balance without smaller text or cramped controls."),
|
||
("Device evidence needs context", "Resolution alone is ambiguous when OS scaling and browser chrome change the usable viewport; recording both native and inner dimensions makes results reproducible."),
|
||
])
|
||
add_callout(doc, "Remaining checkpoint", "Run DV-01 and DV-02 on real computers, attach the named system/application captures, update the test log and regenerate this package.", "FFF5E6")
|
||
add_appendix_page(doc, "RWD-01", "Dashboard at 1920×1080", "responsive-1920x1080-dashboard.png")
|
||
add_appendix_page(doc, "RWD-02", "Dashboard at 1680×1050 after correction", "responsive-1680x1050-dashboard.png")
|
||
add_appendix_page(doc, "RWD-03", "Dashboard at 1920×1200", "responsive-1920x1200-dashboard.png")
|
||
add_appendix_page(doc, "RWD-04A", "Reproduced 1680×1050 card-orphan defect", "responsive-defect-before-1680x1050.png")
|
||
add_appendix_page(doc, "RWD-04B", "Corrected balanced 2×2 summary grid", "responsive-fix-after-1680x1050.png")
|
||
return doc
|
||
|
||
|
||
def build_browsers():
|
||
doc = Document()
|
||
configure_document(doc, "Testing Across Multiple Browsers", "Test using multiple browsers")
|
||
add_masthead(doc, "Testing Across Multiple Browsers", "A consistent browser strategy for Google Chrome, Mozilla Firefox and Microsoft Edge.", "Edge session pending")
|
||
add_section(doc, "Objective", [(None, "Show that the same Learning Planner workflows and responsive layouts behave consistently in Google Chrome, Microsoft Edge and Mozilla Firefox, with reproducible developer-tools evidence and compatibility remediation.")])
|
||
add_section(doc, "Approach", [
|
||
("Common scenario", "Each browser starts from the same seeded dataset and executes persisted goal creation, task search/status, storage reload and overflow checks. This separates browser differences from test-data differences."),
|
||
("Coverage", "Playwright runs the installed Google Chrome channel and a Mozilla Firefox build. A Windows Edge protocol repeats the scenario and captures version, Console, Local Storage and computed Grid evidence."),
|
||
])
|
||
add_section(doc, "Work completed", [
|
||
("Compatibility design", "Used semantic HTML, system fonts, standards-based Grid/Flexbox, visible focus, reduced motion, a `crypto.randomUUID` fallback and validated browser storage."),
|
||
("Developer tools", "Console review checks runtime errors; Application/Storage verifies persistence; computed styles expose breakpoint differences; the accessibility tree verifies useful control names and structure."),
|
||
("Regression path", "A reproduced issue must record environment and expected/actual behaviour, use the smallest standards-based fix and gain an automated or documented manual retest."),
|
||
])
|
||
add_callout(doc, "Compatibility concerns", "Date inputs, font metrics/wrapping, sticky positioning, focus outlines, Grid sizing, downloads/uploads, localStorage and UUID support are explicit checklist items.")
|
||
|
||
doc.add_page_break()
|
||
add_section(doc, "Testing performed", [
|
||
("Google Chrome", "The installed Chrome channel passed the core planning journey, task interaction, overflow check and evidence capture."),
|
||
("Mozilla Firefox", "Firefox passed the same functional and overflow checks; its seeded dashboard was captured for direct comparison."),
|
||
("Microsoft Edge", "The Windows/manual session remains pending because Edge is not installed on this Linux host. The checklist requires the stable version page, app, Console and Local Storage captures."),
|
||
])
|
||
add_section(doc, "Outcome", [(None, "Chrome and Firefox provide completed, matching automated evidence with no application console errors. Edge is not claimed complete: the document becomes assessor-ready only after the genuine Edge session is logged and its appendix screenshots are added.")])
|
||
add_section(doc, "Lessons learned", [
|
||
("Name browsers precisely", "A Chromium engine run helps compatibility, but it is not evidence of the branded Chrome or Edge products. Launching the installed Chrome channel strengthened the result."),
|
||
("Use tools to explain differences", "Computed styles, console messages and stored data are more diagnostic than screenshots alone; together they show whether a problem is layout, runtime or persistence."),
|
||
("Prefer progressive enhancement", "Feature detection and narrow fallbacks are safer than broad polyfills. Add a prefix/polyfill only for a reproduced support gap and retain a regression test."),
|
||
])
|
||
add_callout(doc, "Backwards-compatibility response", "Confirm the support baseline, reproduce in the affected browser, prefer standards and feature detection, add Autoprefixer or a narrowly scoped fallback/polyfill only when needed, then rerun the full scenario.", "EEF0FF")
|
||
add_appendix_page(doc, "BR-01", "Google Chrome seeded dashboard", "browser-chrome-dashboard.png")
|
||
add_appendix_page(doc, "BR-02", "Mozilla Firefox seeded dashboard", "browser-firefox-dashboard.png")
|
||
return doc
|
||
|
||
|
||
def main():
|
||
OUT.mkdir(parents=True, exist_ok=True)
|
||
builds = {
|
||
"01-personal-project-management.docx": build_project_management(),
|
||
"02-multiple-device-testing.docx": build_devices(),
|
||
"03-multiple-browser-testing.docx": build_browsers(),
|
||
}
|
||
for name, doc in builds.items():
|
||
doc.save(OUT / name)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|