- What is the value of
ids after the property assignments? Answer: ["a", "b"]
This question tests a subtle but important distinction between HTML attributes and DOM properties — two things that look similar but behave very differently.
When the browser parses the HTML, the checked attribute on #a is baked into the DOM's initial state. The CSS attribute selector input[checked] targets elements that have this HTML attribute present, regardless of their current state. So #a will always match input[checked] because it was written with checked in the markup — even after you set a.checked = false. Changing the .checked property in JavaScript does not remove the original HTML attribute.
The pseudo-class input:checked, on the other hand, reflects the element's current live state. After a.checked = false and b.checked = true, only #b is currently checked. So #b matches input:checked.
Now the selector "input[checked], input:checked" is a union of both. #a matches the attribute selector, and #b matches the pseudo-class. That gives you ["a", "b"], making C the correct answer.
Looking at the wrong choices: A (["a"]) ignores that #b's live checked state is captured by :checked. B (["b"]) ignores that #a still carries its original HTML attribute. D (["a", "a", "b"]) assumes #a would appear twice — once per selector — but querySelectorAll returns a deduplicated node list, so even if #a matched both selectors, it only appears once.
A good rule of thumb: HTML attributes are static snapshots from the markup; DOM properties reflect live, current state. When both appear in a selector, expect them to capture different elements.
- What is the value of
ids? Answer: ["p1", "p2", "p3"]
When you call document.querySelectorAll() with a selector list like ".y, .x", it's easy to assume the results are ordered by the selector — matching .y elements first, then .x elements. That's the trap this question is testing.
In reality, querySelectorAll always returns elements in document order (the order they appear in the DOM), regardless of how you write the selector. The browser scans the document top to bottom, collects all elements matching any of the provided selectors, and deduplicates automatically. So even though .y appears before .x in ".y, .x", the method still returns p1, p2, and p3 in that sequence — because that's their order in the HTML. The .map() then extracts each element's id, giving you ["p1", "p2", "p3"], making B correct.
A (["p2", "p3", "p1"]) reflects the mistaken belief that results are ordered by selector — .y matches p2 and p3 first, then .x matches p1. That's not how it works.
C (["p2", "p3", "p1", "p2"]) combines both misconceptions: selector-based ordering and duplicating p2 because it matches both classes. querySelectorAll deduplicates, so p2 only appears once.
D (["p1", "p2", "p2", "p3"]) assumes document order but still expects p2 to appear twice — once for .x and once for .y. Again, deduplication prevents this.
A reliable rule to remember: querySelectorAll always returns a deduplicated, document-ordered NodeList, no matter how complex your selector is.
- Which element is assigned to
selected? Answer: No element; the value is null
When chaining CSS pseudo-classes like :nth-of-type() and :not(), it's critical to understand the order of operations — specifically, that :nth-of-type() counts elements before :not() filters them.
Here's what actually happens: section > p:nth-of-type(2) first identifies the second <p> child of <section> by counting all <p> elements in order. That selects the paragraph with id="b" — the hidden one. Then :not([hidden]) checks whether that same element has a hidden attribute. Since #b does have hidden, the combined selector matches nothing, and querySelector returns null. So D is correct.
A is wrong because #a is the first <p>, not the second. :nth-of-type(2) never points to it regardless of the :not() filter.
B is wrong for the opposite reason — it's a common trap. You might think :nth-of-type(2) selects #b and then stop there, but the :not([hidden]) condition immediately disqualifies it, so the full selector yields no match.
C is wrong because of a subtle but important misconception: :nth-of-type() does not re-count after :not() excludes elements. Students often assume that filtering out hidden elements "renumbers" the remaining paragraphs, making #c the new second — but that's not how CSS works.
Study tip: Always remember that :nth-of-type() counts based on DOM position, not on the filtered subset. Think of filtering and counting as two separate steps where counting always happens first.
- Which selector passed to
document.querySelectorAll() returns only the button with ID save? Answer: article.card.featured > button.action:not([disabled])
CSS selector syntax is all about specificity and relationship operators — this question tests whether you can distinguish between descendant (space), child (>), and grouping (,) combinators, plus pseudo-classes and attribute selectors.
To return only the #save button, your selector needs to match a button that is: (1) a direct child of an article with both .card and .featured classes, (2) has class .action, and (3) is not disabled. The #save button satisfies all of these — it sits directly inside a .card.featured article with no wrapper element, and it isn't disabled. That's exactly what D (article.card.featured > button.action:not([disabled])) expresses. The > ensures a direct-child relationship, .card.featured (no space) requires both classes on the same element, and :not([disabled]) excludes the #locked button.
A uses a comma, making it a grouped selector — it matches any article.card plus any .featured > button.action:not([disabled]). That's two separate rules, so it returns far more elements than intended.
B replaces the comma with a space, making .featured a descendant of article.card — but .featured is a class on the article itself, not a child inside it. This selector would match nothing in the given HTML.
C drops the :not([disabled]) and instead targets button.action[disabled], which matches the disabled #locked button — the opposite of what you want.
A handy tip: when you chain classes without a space (.card.featured), both must exist on the same element. A space between them means ancestor/descendant — don't confuse the two.
- What does the final
console.log output? Answer: 3 false 3
When working with the DOM in JavaScript, the critical distinction to understand is the difference between static and live collections. querySelectorAll() returns a static NodeList — a snapshot of matching elements taken at the moment the query runs. It does not update when the DOM changes afterward.
With that in mind, trace through the code. cached captures all three .item elements at query time, so cached.length is 3 — and stays 3 forever, regardless of what happens next. Then the class "item" is removed from element #a, and a new <li class="item"> is appended to the list. Neither of these DOM mutations affects cached at all.
Now the three logged values: cached.length is 3 (the static snapshot still holds all three original elements). cached[0].matches(".item") checks whether the first cached element — which is #a — currently matches .item. Since its class was just removed, this returns false. Finally, a fresh querySelectorAll(".item") is called: #a is gone from the class, but the new <li> was added, leaving exactly 3 matching elements. Output: 3 false 3, which is A.
B is wrong because it assumes #a still has the .item class, ignoring the classList.remove call. C is wrong because it assumes cached.length reflects DOM changes — it doesn't; querySelectorAll returns a static snapshot. D is wrong for the same reason as C and also miscounts the live query result.
Remember this rule: querySelectorAll = static snapshot, getElementsByClassName = live HTMLCollection. Knowing which is which will save you on any DOM-manipulation question.
- Which sequence contains the IDs of the elements in
links, in collection order? Answer: a, c, d
When you see querySelectorAll with a complex selector, break it into its two distinct parts: the scope and the target.
The selector "#menu > li:not(.disabled) a.active" works in two stages. First, #menu > li:not(.disabled) identifies direct children of #menu that lack the disabled class — that gives you the <li> containing A, the <li> containing C (wrapped in a <span>), and the <li> containing the nested <ul> with D. The <li> containing B is excluded by :not(.disabled). Second, a.active matches any <a> with class active that is a descendant (not just a direct child) of those qualifying <li> elements. Crucially, querySelectorAll evaluates the full selector against the entire document — the > only constrains the <li> selection, not how deep the a.active can be. So A (direct child anchor), C (inside a <span>), and D (inside a nested <ul>) all qualify, making **A (a, c, d)** correct.
Choice B (a, c) incorrectly excludes D, likely because you might assume the nested <ul> creates a boundary — it doesn't. Choice C (a, b, c) includes B, ignoring that its parent <li> is filtered out by :not(.disabled). Choice D (a, b, c, d) ignores both the :not(.disabled) filter and the nesting depth concern simultaneously.
Study tip: Remember that CSS descendant matching in querySelectorAll has no depth limit — once a parent element passes the left-hand filter, any nested descendant matching the right-hand part is included.
- What does
found reference? Answer: The div whose ID is child
When you call querySelector() on a specific element (rather than on document), it searches only within that element's descendants — but the scope of matching still follows normal CSS rules. The key detail here is that querySelector always returns the first matching element it finds in document order.
Here, root.querySelector(".box") searches inside the #root div for any element with the class box. The HTML has two elements with that class: #root itself and #child. However, querySelector only searches descendants of the element it's called on — #root is not its own descendant, so it's excluded from the search. That leaves #child as the first (and only) matching descendant, making B the correct answer — found references the div with ID child.
A is wrong because #root is the element you're calling querySelector on, not a descendant of itself. Even though it has the class box, it cannot match against itself in this search — a common misconception students have about scoped queries.
C is wrong because there is indeed a matching descendant (#child), so the result is not null. querySelector only returns null when no match exists at all.
D is wrong because querySelector always returns a single element or null, never a NodeList. If you want all matches, you'd use querySelectorAll, which would return a NodeList here.
A useful mental model: think of element.querySelector() as "search inside this element, give me the first hit." The element itself is never included in its own search.
- Which expression reliably selects the displayed
div by using querySelector() and the value in id? Answer: document.querySelector("#" + CSS.escape(id))
When working with querySelector(), you need to understand that CSS selectors have special characters — colons (:), question marks (?), and others carry syntactic meaning in CSS. If your selector string contains these characters unescaped, the browser's CSS parser will misinterpret them, throwing an error or returning null instead of finding your element.
The built-in CSS.escape() method exists precisely to solve this: it takes a string and escapes any characters that would otherwise break a CSS selector. Since the id value "order:7?draft" contains both : and ?, you need to escape it before embedding it into a selector string. The correct approach is to build the # prefix first (since that's a CSS selector symbol, not part of the ID itself), then escape only the ID value — giving you "#" + CSS.escape(id). That's exactly what A does, making it the reliable solution.
B fails because it concatenates # and the raw id string without escaping. The colon and question mark in "order:7?draft" confuse the CSS parser, causing the selector to be invalid.
C applies CSS.escape() to the entire string "#" + id, which escapes the # character too — turning it into \#, a literal hash symbol rather than an ID selector prefix. This breaks the selector's meaning entirely.
D uses . instead of #, which targets a CSS class, not an ID. Even with proper escaping, it selects the wrong attribute.
A good rule of thumb: always escape only the value portion of a selector, not the selector syntax characters like # or . that you intentionally add.
- Which expression returns a
NodeList containing only element x? Answer: panel.querySelectorAll(':scope > .item[data-ready="yes"]')
When you call querySelectorAll on a specific element (rather than document), you need to understand two things: how CSS combinators work, and what the :scope pseudo-class does. :scope refers to the element the query is being called on — in this case, #panel. Without it, selectors can behave unexpectedly when scoped to a non-document element.
Option A — ':scope > .item[data-ready="yes"]' — is correct. The > is the child combinator, meaning it only matches direct children of #panel. Both x and y have class="item" and data-ready="yes", but y is nested inside .wrapper, making it a grandchild of #panel. So only x qualifies, giving you exactly the NodeList you want.
Option B — '.item[data-ready="yes"]' — is a trap. Without :scope, this matches .item[data-ready="yes"] anywhere inside #panel, returning both x and y (since y is still a descendant, just not a direct child).
Option C — ':scope .item[data-ready="yes"]' — uses :scope correctly but with a descendant combinator (a space), not a child combinator. This still matches all descendants, so x and y are both returned.
Option D — ':scope > .item, [data-ready="yes"]' — is a comma-separated selector list. The second part, [data-ready="yes"], matches any descendant with that attribute, pulling in both x and y again.
As a quick tip: whenever you're querying from a specific element and need strict depth control, always pair :scope with the > child combinator — that combination is your precision tool.
- What value is assigned to
result? Answer: The string "SyntaxError" from the exception
When working with document.querySelectorAll(), you need to understand that it validates the CSS selector string before searching the DOM. If the selector is syntactically invalid, the method throws a SyntaxError exception immediately — it never even attempts to find elements.
In this code, ".card, :not(" is an incomplete, malformed CSS selector — the :not() pseudo-class is never closed with a matching parenthesis. The browser's CSS parser detects this structural error and throws a SyntaxError. Because the try/catch block catches that error, result is assigned error.name, which is the string "SyntaxError". So C is correct.
A is wrong because querySelectorAll never successfully queries the DOM at all — the invalid selector causes it to throw before any element matching occurs, so no NodeList is returned. B falls into the same trap: an empty NodeList would only be returned if the selector were valid but matched nothing. Here, the problem is the selector itself, not the absence of matching elements. D is wrong because a TypeError typically occurs when you call a method on the wrong type (like calling a function on null) — the issue here is a bad CSS string, not a type mismatch.
A useful pattern to remember: querySelectorAll is "all or nothing" on selector validity. Any syntax error in the selector string throws immediately, regardless of how many valid parts came before it. When you see try/catch wrapped around a DOM query, check whether the selector could be malformed — that's almost always what's being tested.