More from Confessions of a Code Addict
Write faster code by understanding how it flows through your CPU
Discover why 'if not mylist' is twice as fast as 'len(mylist) == 0' by examining CPython's VM instructions and object memory access patterns.
A Live, Interactive Course for Systems Engineers
A personal guide to the most useful books for understanding operating systems
More in programming
There's this thing in Python that always trips me up. It's not that tricky, once you know what you're looking for, but it's not intuitive for me, so I do forget. It's that shadowing a variable can sometimes give you an UnboundLocalError! It happened to me last week while working on a workflow engine with a coworker. We were refactoring some of the code. I can't share that code (yet?) so let's use a small example that illustrates the same problem. Let's start with some working code, which we had before our refactoring caused a problem. Here's some code that defines a decorator for a function, which will trigger some other functions after it runs. def trigger(*fns): """After the decorated function runs, it will trigger the provided functions to run sequentially. You can provide multiple functions and they run in the provided order. This function *returns* a decorator, which is then applied to the function we want to use to trigger other functions. """ def decorator(fn): """This is the decorator, which takes in a function and returns a new, wrapped, function """ fn._next = fns def _wrapper(): """This is the function we will now invoke when we call the wrapped function. """ fn() for f in fn._next: f() return _wrapper return decorator The outermost function has one job: it creates a closure for the decorator, capturing the passed in functions. Then the decorator itself will create another closure, which captures the original wrapped function. Here's an example of how it would be used[1]. def step_2(): print("step 2") def step_3(): print("step 3") @trigger(step_2, step_3) def step_1(): print("step 1") step_1() This prints out step 1 step 2 step 3 Here's the code of the wrapper after I made a small change (omitting docstrings here for brevity, too). I changed the for loop to name the loop variable fn instead of f, to shadow it and reuse that name. def decorator(fn): fn._next = fns def _wrapper(): fn() for fn in fn._next: fn() And then when we ran it, we got an error! UnboundLocalError: cannot access local variable 'fn' where it is not associated with a value But why? You look at the code and it's defined. Right out there, it is bound. If you print out the locals, trying to chase that down, you'll see that there does not, in fact, exist fn yet. The key lies in Python's scoping rules. Variables are defined for their entire scope, which is a module, class body, or function body. If you define a variable within a scope, anywhere inside a function, then that variable has that name as its own for the entire scope. The docs make this quite clear: If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations. See the FAQ entry on UnboundLocalError for examples. This comes up in a few other places, too. You can use a loop variable anywhere inside the enclosing scope, for example. def my_func(): for x in [1,2,3]: print(x) # this will still work! # x is still defined! print(x) So once I saw an UnboundLocalError after I'd shadowed it, I knew what was going on. The name was used by the local for the entire function, not just after it was initialized! I'm used to shadowing being the idiomatic thing in Rust, then had to recalibrate for writing Python again. It made sense once I remembered what was going on, but I think it's one of Python's little rough edges. This is not how you'd want to do it in production usage, probably. It's a somewhat contrived example for this blog post. ↩
A few weeks ago, I ran a pair programming / mentoring session with someone who reached out to me because they felt they could use some support. When I first saw the code they wrote, I was pretty impressed. Sure, there were some things I would have done differently, but most of that was personal preference, not a matter of my way being better than their way objectively. Instead of working on their code directly, instead, we therefore decided to build up some test code together from zero, discussing and applying good programming principles and patterns along the way. As the tests were using Playwright in TypeScript, and were heavily oriented towards using the graphical user interface, we decided to start building a Page Object-based structure for a key component in their application. This component was a UI component that enabled an end user to create a report in the system. The exact type of system or even the domain itself isn’t really important for the purpose of this blog post, by the way. The component looked somewhat like this, heavily simplified: At the top, there was a radiobutton with three options that selected different report layouts. Every report layout consists of multiple form fields, and most form fields are text areas plus lock buttons that open a dropdown-like structure where you can edit the permissions for that field by selecting one or more roles that can view the contents of that text field (this is a privacy feature). And of course, there’s a save button to save the report, as well as a print button. The actual UI component had a few other types of components, but for the sake of brevity, let’s stick to these for now. Iteration 0 - creating an initial Page Object My approach whenever I start from scratch, either on my own or when working with someone else, is to take small steps and gradually introduce complexity. It might be tempting to immediately create a Page Object containing fields for all the elements and methods to interact with them, but that is going to get messy very quickly. Instead, we started with the simplest Page Object we could think of: one that allowed us to create a standard report, without considering the lock buttons to set permissions. Let’s assume that a standard report consists of only a title and a summary text field. The first iteration of that Page Object turned out to look something like this: export class StandardReportPage { readonly page: Page; readonly radioSelectStandard: Locator; readonly textfieldTitle: Locator; readonly textfieldSummary: Locator; readonly buttonSaveReport: Locator; readonly buttonPrintReport: Locator; constructor(page: Page) { this.page = page; this.radioSelectStandard = page.getByLabel('Standard report'); this.textfieldTitle = page.getByPlaceholder('Title'); this.textfieldSummary = page.getByPlaceholder('Summary'); this.buttonSaveReport = page.getByRole('button', { name: 'Save' }); this.buttonPrintReport = page.getByRole('button', { name: 'Print' }); } async select() { await this.radioSelectStandard.click(); } async setTitle(title: string) { await this.textfieldTitle.fill(title); } async setSummary(summary: string) { await this.textfieldSummary.fill(summary); } async save() { await this.buttonSaveReport.click(); } async print() { await this.buttonPrintReport.click(); } } which makes the test using this Page Object look like this: test('Creating a standard report', async ({ page } ) => { const standardReportPage = new StandardReportPage(page); await standardReportPage.select(); await standardReportPage.setTitle('My new report title'); await standardReportPage.setSummary('Summary of the report'); await standardReportPage.save(); await expect(page.getByTestId('standard-report-save-success')).toBeVisible(); }); Iteration 1 - grouping element interactions My first question after we implemented and used this Page Object was: ‘how do you feel about the readability of this test?’. Of course, we just wrote this code, and it’s a small example, but imagine you’re working with Page Objects that are all written like this, and offer many more element interactions. This will quickly lead to very procedural test code ‘enter this, enter that, click here, check there’ that doesn’t show the intent of the test very clearly. In other words, this coding style does not really do a great job of hiding the implementation of the page (even when it hides the locators) and focusing only on behaviour. To improve this, I suggested grouping element interactions that form a logical end user interaction together in a single method and expose that. When I read or write a test, I’m not particularly interested in the sequence of individual element interactions I need to execute to perform a higher-level action. I’m not interested in ‘filling a text field’ or ‘clicking a button’, I’m interested in ‘creating a standard report’. This led us to refactor the Page Object into this: export class StandardReportPage { readonly page: Page; readonly radioSelectStandard: Locator; readonly textfieldTitle: Locator; readonly textfieldSummary: Locator; readonly buttonSaveReport: Locator; readonly buttonPrintReport: Locator; constructor(page: Page) { this.page = page; this.radioSelectStandard = page.getByLabel('Standard report'); this.textfieldTitle = page.getByPlaceholder('Title'); this.textfieldSummary = page.getByPlaceholder('Summary'); this.buttonSaveReport = page.getByRole('button', { name: 'Save' }); this.buttonPrintReport = page.getByRole('button', { name: 'Print' }); } async select() { await this.radioSelectStandard.click(); } async create(title: string, summary: string) { await this.textfieldTitle.fill(title); await this.textfieldSummary.fill(summary); await this.buttonSaveReport.click(); } async print() { await this.buttonPrintReport.click(); } } which in turn made the test look like this: test('Creating a standard report', async ({ page } ) => { const standardReportPage = new StandardReportPage(page); await standardReportPage.select(); await standardReportPage.create('My new report title', 'Summary of the report'); await expect(page.getByTestId('standard-report-save-success')).toBeVisible(); }); Much better already when it comes to readability and ‘expose behaviour, hide implementation’. Doing exactly this is not something that is unique to UI automation, or even to test automation in general, by the way. This principle is called encapsulation, and it is one of the fundamental principles of object-oriented programming. It is a principle that is very useful to know when you’re writing test code, if you want to keep your code readable, that is. Iteration 2 - Adding the ability to set permissions on a form field For our next step, we decided to introduce the ability to set the access permissions for every text field. As explained and shown in the graphical representation of the form at the top of this post, every form field in the standard form has an associated lock button that opens a small dialog where the user can select which user roles can and cannot see the report field. Our initial idea was to simply add additional fields in the Page Object representing the standard report. However, that would lead to a lot of repetitive work and to the standard report having a lot of fields containing element locators. So, we decided to see if we could consider the combination of a report text field and the associated permission lock button as a Page Component, i.e., a separate class that encapsulates the behaviour of a group of related elements on a specific page. Setting this up in a reusable manner will be a lot easier when the HTML for these Page Components has the same structure across the entire application. The good news is that this is often the case, especially when frontend designers and developers design and implement frontends using tools like Storybook. So, the relevant part of the HTML for the standard form might look like this (again, simplified): <div id="standard_form"> <div data-testid="form_field_subject"> <div data-testid="form_field_subject_textfield"></div> <div data-testid="form_field_subject_lock"></div> </div> <div data-testid="form_field_summary"> <div data-testid="form_field_summary_textfield"></div> <div data-testid="form_field_summary_lock"></div> </div> </div> An example reusable Page Component class might then look something like this: export class ReportFormField { readonly page: Page; readonly textfield: Locator; readonly buttonLockPermissions: Locator; constructor(page: Page, formFieldName: string) { this.page = page; this.textfield = page.getByTestId(`${formFieldName}_textfield`); this.buttonLockPermissions = page.getByTestId(`${formFieldName}_lock`); } async complete(text: string, roles: string[]) { await this.textfield.fill(text); await this.buttonLockPermissions.click(); // handle setting permissions for the form field } } Note how the constructor of this Page Component class uses (in fact, relies on) the predictable, repetitive structure of the component in the application and the presence of data-testid attributes. If your components do not have these, find a way to add them, or find another generic way to locate the individual elements in the component on the page. Now that we have defined our Page Component class, we need to define the relationship between these Page Components and the Page Object that contains them. In the past, my choice would default to creating base Page classes that would contain the reusable Page Components, as well as other utility methods. The more specific Page Object would then inherit from these base Pages, allowing them to use the methods defined in the parent base Page class. Almost invariably, at some point that would lead to very messy base Page classes, with lots of fields and methods in it that were only tangentially related, at best. The cause of this mess? Me not thinking clearly about the type of the relation between different Page Objects and Components. You see, creating base classes and using inheritance for reusability creates ‘is-a’ relations. These are useful when the relation between objects is of an ‘is-a’ nature. However, in our case, there is no ‘is-a’ relation, there is a ‘has-a’ relation. A Page Object has a certain Page Component. In other words, we need to define the relationship a different way, and that’s by using composition instead of inheritance. We define Page Components as components of our Page Objects, which makes for a far more natural relationship between the two, and for code that is way more clearly structured: export class StandardReportPage { readonly page: Page; readonly radioSelectStandard: Locator; readonly reportFormFieldTitle: ReportFormField; readonly reportFormFieldSummary: ReportFormField; readonly buttonSaveReport: Locator; readonly buttonPrintReport: Locator; constructor(page: Page) { this.page = page; this.radioSelectStandard = page.getByLabel('Standard report'); this.reportFormFieldTitle = new ReportFormField(this.page, 'title'); this.reportFormFieldSummary = new ReportFormField(this.page, 'summary'); this.buttonSaveReport = page.getByRole('button', { name: 'Save' }); this.buttonPrintReport = page.getByRole('button', { name: 'Print' }); } async select() { await this.radioSelectStandard.click(); } async create(title: string, summary: string, roles: string[]) { await this.reportFormFieldTitle.complete(title, roles); await this.reportFormFieldSummary.complete(summary, roles); await this.buttonSaveReport.click(); } async print() { await this.buttonPrintReport.click(); } } Reading this code feels far more natural than cramming everything into one or more parent classes c.q. base page objects. Lesson learned here: the way objects are related in your code should reflect the relationship between these objects in real life, that is, in your application. Iteration 3 - What about the other report types? The development and refactoring steps we have gone through so far led us to a point where we were pretty happy with the code. However, we still only have Page Objects for a single type of form, and as you have seen in the sketch at the top of this blog post, there are different types of forms. How do we deal with those? Especially when we know that these forms share some components and behaviour, but not all of them? It is tempting to immediately jump to conclusions and start throwing patterns and structures at the problem, but in pair programming sessions like this, I typically try and avoid finding and implementing the ‘final’ solution right away. Why? Because the best learning is done when you see (or create, in this case) a suboptimal situation, discuss the problems with that situation, investigate potential solutions and only then implement them. Sure, it will take longer, initially, but this is made up for in spades with a much better understanding of what suboptimal code looks like and how to improve it. So, first we create separate classes for individual report types, each similar to the implementation for the standard report we created before. Here is an example for an extended report, containing more form fields (well, just one more, but you get the idea): export class ExtendedReportPage { readonly page: Page; readonly radioSelectExtended: Locator; readonly reportFormFieldTitle: ReportFormField; readonly reportFormFieldSummary: ReportFormField; readonly reportFormFieldAdditionalInfo: ReportFormField; readonly buttonSaveReport: Locator; readonly buttonPrintReport: Locator; constructor(page: Page) { this.page = page; this.radioSelectExtended = page.getByLabel('Extended report'); this.reportFormFieldTitle = new ReportFormField(this.page, 'title'); this.reportFormFieldSummary = new ReportFormField(this.page, 'summary'); this.reportFormFieldAdditionalInfo = new ReportFormField(this.page, 'additionalInfo'); this.buttonSaveReport = page.getByRole('button', { name: 'Save' }); this.buttonPrintReport = page.getByRole('button', { name: 'Print' }); } async select() { await this.radioSelectExtended.click(); } async create(title: string, summary: string, additionalInfo: string, roles: string[]) { await this.reportFormFieldTitle.complete(title, roles); await this.reportFormFieldSummary.complete(summary, roles); await this.reportFormFieldAdditionalInfo.complete(additionalInfo, roles); await this.buttonSaveReport.click(); } async print() { await this.buttonPrintReport.click(); } } Obviously, there’s a good amount of duplication between this class and the Page Object for the standard report. What to do with them? Contrary to the situation with the Page Components, it is a good idea to reduce the duplication by creating a base report Page Object here. We’re talking about creating an ‘is-a’ relationship (inheritance) here, not a ‘has-a’ relation (composition). A standard report is a report. That means that in this case, we can, and we should, create a base report Page Object, move some (or maybe even all) of the duplicated code there, and have the specific report Page Objects derive from that base report class. My recommendation here is to make the base report Page Object an abstract class to prevent people from instantiating it directly. This leads to more expressive and clear code, as we can only instantiate the concrete report subtype, which will make it immediately clear to the reader of the code what type of report they’re dealing with. In the abstract class, we declare the elements that are shared between all reports. This applies to methods, but also to web elements that appear in all report types. This is what the abstract base class might look like: export abstract class ReportBasePage { readonly page: Page; readonly reportFormFieldTitle: ReportFormField; readonly reportFormFieldSummary: ReportFormField; readonly buttonSaveReport: Locator; readonly buttonPrintReport: Locator; abstract readonly radioSelect: Locator; protected constructor(page: Page) { this.page = page; this.reportFormFieldTitle = new ReportFormField(this.page, 'title'); this.reportFormFieldSummary = new ReportFormField(this.page, 'summary'); this.buttonSaveReport = page.getByRole('button', { name: 'Save' }); this.buttonPrintReport = page.getByRole('button', { name: 'Print' }); } async select() { await this.radioSelect.click(); } async print() { await this.buttonPrintReport.click(); } } and a concrete class for the standard report, implementing the abstract class now looks like this: export class ExtendedReportPage extends ReportBasePage { readonly page: Page; readonly radioSelect: Locator; readonly reportFormFieldAdditionalInfo: ReportFormField; constructor(page: Page) { super(page); this.page = page; this.radioSelect = page.getByLabel('Extended report'); this.reportFormFieldAdditionalInfo = new ReportFormField(this.page, 'additionalInfo'); } async create(title: string, summary: string, additionalInfo: string, roles: string[]) { await this.reportFormFieldTitle.complete(title, roles); await this.reportFormFieldSummary.complete(summary, roles); await this.reportFormFieldAdditionalInfo.complete(additionalInfo, roles); await this.buttonSaveReport.click(); } } The abstract class takes care of the methods that are shared between all reports, such as the print() and the select() methods, It also defines what elements and methods should be implemented by the implementing concrete classes. For now, that’s only the radioSelect locator. Note that at the moment, because the data required for the different types of reports is not the same, we cannot yet add an abstract select(): void method requirement, that all report Page Objects should implement, to our abstract class. This is a temporary drawback and one that we will address in a moment. Also note that the test code doesn’t change, but we can now create both a standard report and an extended report that, behind the scenes, share a significant amount of code. Definitely a step in the right direction. Iteration 4 - Dealing with test data Our tests already look pretty good. They are easy to read, and the way the code is structured aligns with the structure of the parts of the application they’re representing. Are we done yet? Well, maybe. As a final improvement to our tests, let’s have a look at the way we handle our test data. Right now, the test data we use in our test methods is simply an unstructured collection of strings, integers, boolean and so on. For small tests and a simple domain that is easy to understand, you might get away with this, but as soon as your test suite grows and your domain becomes more complex, this will get confusing. What does that string value represent exactly? Why is that variable a boolean and what happens if it is set to true (or false)? This is where test data objects can help out. Test data objects are simple classes, often nothing more fancy than a Data Transfer Object (DTO), that represent a domain entity. In this situation, that domain entity might be a report, for example. Having types that represent domain entities greatly improves the readability of our code, it will make it much easier to understand what exactly we’re doing here. The implementation of these test data objects is often straightforward. In TypeScript, we can use a simple interface for this purpose. I chose to create one ReportContent class that contains the data for all of our report types. As they diverge, I might choose to refactor these into separate interfaces, but for now, this is fine. Also, defining this test data object has the additional benefit of allowing me to move the definition of the create() method for the different report Page Objects to the abstract base class, a step that we were unable to perform previously. This is what my interface looks like: export interface ReportContent { title: string; summary: string; additionalInfo?: string; roles: string[]; } The additionalInfo field is marked as optional, as it only appears in an extended report, not in a standard report. In some cases, to further improve flexibility and readability of our code, we might add a builder or a factory that helps us create instances of our test data objects using a fluent syntax. This also allows us to set sensible default values for properties to avoid having to assign the same values to these properties in every test. In this specific case, that’s not really necessary, because object creation based on an interface in TypeScript is really straightforward, and our ReportContent object is small, anyway. Your mileage may vary. Now that we have defined a type for our report data, we can change the signature and the implementation of the create() methods in our Page Objects to use this type. Here’s an example for the extended report: async create(report: ReportContent) { await this.reportFormFieldTitle.complete(report.title, report.roles); await this.reportFormFieldSummary.complete(report.summary, report.roles); await this.reportFormFieldAdditionalInfo.complete(report.additionalInfo, report.roles); await this.buttonSaveReport.click(); } and we can now add the following line to the abstract ReportBasePage class: abstract create(report: ReportContent): void; to enforce all report Page Objects to implement a create() method that takes an argument of type ReportContent. We can do the same for other test data objects. Oh, and if you’re storing your tests in the same repository as your application code, these test data objects might even exist already, in which case you might be able to reuse them. This is definitely worth checking, because why would we reinvent the wheel? That was a lot of work, but it has led to code that is, in my opinion, well-structured and easy to read and maintain. As this blog post has hopefully shown, it is very useful to have a good working knowledge of common object-oriented programming principles and patterns when you’re writing test code. This is especially true for UI automation, but many of the principles we have seen in this blog post can be applied to other types of test automation, too. There are many other patterns out there to explore. This blog post is not an attempt to list them all, nor does it show ‘the one true way’ of writing Page Objects. Hopefully, though, it has shown you my thought process when I write test automation code, and how understanding fundamentals of object-oriented programming helps me do this better. A massive ‘thank you’ to Olena for participating in the pair programming session I discussed and for reviewing this blog post. I really appreciate it.
The Ware for March 2025 is shown below. I was just taking this thing apart to see what went wrong, and thought it had some merit as a name that ware. But perhaps more interestingly, I was also experimenting with my cross-polarized imaging setup. This is a technique a friend of mine told me about […]
Beware! This post includes spoilers! I recently built an escape room game called CSScape Room. This isn’t my first JavaScript-free web game, but HTML and CSS have evolved significantly since my previous attempts, with newer additions allowing for more complex selectors and native interactions. Rather than saving this idea for a game jam, I built it purely for fun, which freed me from theme constraints and time pressure. I’ve enjoyed escape room games since childhood, and it was nostalgic to recreate that experience myself. This project pushed my artistic limits while challenging me to design puzzles and translate them into complex HTML and CSS. The learning process was fun, challenging, and sometimes tedious—mostly through trial and error. Process My creative process isn’t linear—it’s a blend of designing, puzzle creation, and coding that constantly influences each other. I frequently had to redesign or recode elements as the project evolved. There was also that time I accidentally deleted half my CSS because I wasn’t backing up to GitHub... lesson learned! 😬 This might sound chaotic, and honestly, it was. If you’re wondering where to start with a project like this, I began by prototyping the room navigation system. I figured that was the minimum viable feature—if I couldn’t make that work, I’d abandon the project. The solution I eventually found seems simple in retrospect, but I went through several iterations to discover it. This flexible approach makes sense for my creative projects. As I build something, both the in-progress work and my growing skills inevitably influences the entire project. I’m comfortable with this non-linear process—it also suits my ADHD brain, where I tend to lose interest if I work on the same thing for too long. Artwork I’d wanted to design a pixel art-styled game for some time but never felt confident enough to attempt it during a game jam because of the learning curve. I watched tutorials from Adam Yunis and Mort to get a crash course in pixel art best practices. Initially, progress was slow. I had to figure out 2D perspective with vanishing points, determine a color palette, practice shading techniques, and decide how much detail to include. While I tried to adhere to pixel art “rules,” I definitely broke some along the way. One challenge I set for myself was using only 32 colors to capture the feeling of an older gaming console. Once I got comfortable with shading and dithering, working within this constraint became easier. An added benefit to using 32 colors was it resulted in smaller image sizes—the game’s 79 images account for only about 25% of the total payload. I attempted to design sprites using dimensions in multiples of eight, but I’ll admit I became less strict about this as the project progressed. At a certain point, I was struggling enough with the color and styling limitations that this guideline became more of a starting point than a rule. I considered creating my own font, but after exhausting myself with all the artwork, I opted for Google’s PixelifySans instead. Almost all animation frames were individually drawn (except for the “one” TV animation). This was tedious, but I was determined to stay true to old-school techniques! I did use CSS to streamline some animations—for instance, I used animation-direction: alternate on the poster page curl to create a palindrome effect, halving the number of required sprites. Mechanics Like my previous game Heiro, this project primarily uses checkbox and radio button mechanics. However, the addition of the :has() pseudo-selector opened up many more possibilities. I also utilized the popover API to create more detailed interactions. Checkbox and Radio Selection Triggering interactions by toggling checkboxes and radio buttons isn’t new, but the :has() selector is a game-changer! Before this existed, you had to structure your markup so interactive elements were siblings. The :has() selector makes this far more flexible because you no longer need to rely on a specific HTML structure. #element { display: none; } :has(#checkbox:checked) #element { display: block; } Using this pattern, :has() looks for #checkbox anywhere on the page, meaning you don’t have to rely on #checkbox, its corresponding <label>, or #element being siblings. The markup structure is no longer a constraint. Most of this game functions on toggling checkboxes and radios to unlock, collect, and use items. Navigation I almost gave up on the current implementation, and used basic compass notation to avoid visual transitions between directions. After several failed attempts, I found a solution. The tricky part was determining how to transition into a direction from either left or right, depending on which arrow was clicked. My solution is conceptually simple but difficult to explain! First, I used radio buttons to determine which direction you’re facing (since you can only face one direction at a time). Second, I needed a way to determine if you’re entering a direction from west or east. This required eight radio buttons—two for each direction. For example, if you’re facing east (having come from facing north), you have two possible directions to go: west (returning to face north) or east (to face south). I needed to make the radio buttons visible that would take you north from east, and south from west. The CSS looks something like this: :has(#east-from-west:checked) :is( [for="south-from-west"], [for="north-from-east"]) { display: block; } This pattern was implemented for each direction, along with animations to ensure each room view slid in and out correctly. Zooming In I initially focused so much on checkbox mechanics that I assumed I’d need the same approach for zooming in on specific areas. Then I had a "Duh!" moment and realized the popover API would be perfect. Here’s the basic markup for looking at an individual book: <button popovertarget="book">Zoom in</button> <div id="book" popover> <!-- Book content goes here --> <button popovertarget="book" popovertargetaction="hide">Close</button> </div> Turning the Lights Off I procrastinated on implementing this feature because I thought I’d need to create darkened variations of all artwork. I don’t recall what inspired me to try blend modes, but I’m glad I did—the solution was surprisingly simple. When the light switch checkbox is toggled, a <div> becomes visible with a dark background color and mix-blend-mode: multiply. This multiplies the colors of the blending and base layers, resulting in a darker appearance. Playing the Crossword This required surprisingly complex CSS. Each square has three letters plus a blank tile, meaning four radio buttons. The :checked letter has a z-index of 3 to display above other letters, but also has pointer-events: none so clicks pass through to the next letter underneath (with z-index: 2). The remaining tiles have a z-index of 1. The CSS becomes even trickier when the last tile is :checked, requiring some creative selector gymnastics to target the first radio button in the stack again. Tools I created all artwork using Aseprite, which is specifically designed for pixel art. I probably only used a fraction of its features, and I’m not sure it actually made my life easier—it might have made things more difficult at times. I’m not giving up on it yet, though. I suspect I’ll occasionally discover features that make me think, “Oh, that’s way easier than what I was doing!” I started coding with basic HTML and CSS but eventually found navigation difficult with such a long HTML file. It also became tedious writing the same attributes for every <img /> element. I migrated the project to Eleventy to improve organization and create custom shortcodes for simplifying component creation. I used the html-minifier-terser npm package, which integrates well with Eleventy. I chose native CSS over Sass for several reasons: CSS now has native nesting for better organization and leaner code CSS has built-in variables HTTP/2 handles asset loading efficiently, eliminating the major benefit of bundling CSS files The game uses 12 CSS files with 12 <link rel="stylesheet" /> tags. The only Sass feature I missed was the ability to loop through style patterns for easier maintenance, but this wasn’t a significant issue. The game is hosted on GitHub Pages. During deployment, it runs an npm command to minify CSS using Lightning CSS. I mentioned accidentally deleting half my CSS earlier—this happened because I initially used Eleventy’s recommended approach with the clean-css npm package. I strongly advise against using this! This package doesn’t work with native CSS nesting. While losing code was frustrating, I rewrote much of it more efficiently, so there was a silver lining. Nice to Haves I initially wanted to make this game fully accessible, but the navigation system doesn’t translate well for screen reader users. I tried implementing a more compass-like navigation approach for keyboard users, but it proved unreliable and conflicted with the side-to-side approach. Adding text labels for interactive elements was challenging because you can’t track the :focus state of a <label> element. While you can track the :focus of the corresponding <input />, it wasn’t consistently reliable. The main keyboard accessibility issue is that the game exists as one long HTML page. When you navigate to face a different direction, keyboard focus remains elsewhere on the page, requiring extensive tabbing to reach navigation elements or item selection. I ultimately decided to make the game deliberately inaccessible by adding tabindex="-1" to all keyboard-accessible elements. I’d rather users recognize immediately that they can’t play with assistive technology than become frustrated with a partially broken experience. Sound would have been a nice addition, but I encountered the same issues as with my previous game Heiro. You can toggle the visibility of an <embed> element, but once it’s visible, you can’t hide it again—meaning there’s no way to toggle sound on and off. Conclusion CSScape Room was a fun but exhausting four-month project. It began as an experiment to see if creating a JavaScript-free escape room was possible—and the answer is definitely yes. I’ve only touched on some aspects here, so if you’re interested in the technical details, check out the source code on GitHub. Finally, I’d like to thank all my playtesters for their valuable feedback!
Everyone wants the software they work on to produce quality products, but what does that mean? In addition, how do you know when you have it? This is the longest single blog post I have ever written. I spent four decades writing software used by people (most of the server