Skip to content

Instantly share code, notes, and snippets.

@warrenseine
Last active September 17, 2026 09:33
Show Gist options
  • Select an option

  • Save warrenseine/e293fc70db61eb7b00b951c2482e955f to your computer and use it in GitHub Desktop.

Select an option

Save warrenseine/e293fc70db61eb7b00b951c2482e955f to your computer and use it in GitHub Desktop.
Collapse test files in Linear reviews
// ==UserScript==
// @name Collapse test files in Linear reviews
// @namespace http://tampermonkey.net/
// @version 2026-09-17
// @description Collapse test files in Linear reviews
// @author Warren Seine
// @match https://linear.app/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=linear.app
// @run-at document-idle
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Basenames to collapse: test_*.py and *.test.ts / *.test.tsx.
const TEST_FILE = /(?:^test_.*\.py|\.test\.tsx?)$/;
// Anything under a tests directory counts too, whatever it is called —
// conftest.py, fixtures, helpers.
const TEST_DIRECTORY = /(?:^|\/)tests\//;
// The Diff tab of a review.
const DIFF_TAB = /^\/[^/]+\/review\/[^/]+\/changes\b/;
// Only expanded files carry this label; a collapsed one reads "Expand file
// contents", so the selector doubles as the state check.
const COLLAPSE = 'button[aria-label="Collapse file contents"]';
const EXPAND = 'Expand file contents';
// The path sits on a copy-to-clipboard control in the file header.
const PATH = '[aria-label^="Copy path"]';
// One file per pass, and the poll doubles as the pace. Collapsing several in
// the same tick loses all but the first few: the state update is batched and
// the re-render puts back every file it has not committed yet.
const PACE = 120;
// Files the reader expanded by hand are theirs — never collapse them again.
const manual = new Set();
// Linear re-renders rows as the list scrolls, so collapsing is retried rather
// than done once. The cap per visit stops the retry from becoming a loop.
const MAX_COLLAPSES = 10;
let attempts = new Map();
let tab = null;
function filePath(toggle) {
// The header is a few generic wrappers up from the button.
for (let el = toggle; el; el = el.parentElement) {
const copy = el.querySelector(PATH);
if (copy) return copy.getAttribute('aria-label').replace(/^Copy path:\s*/, '').trim();
}
return null;
}
function isTestFile(path) {
return TEST_DIRECTORY.test(path) || TEST_FILE.test(path.split('/').pop());
}
// Collapses the first test file still expanded. A file is rendered more than
// once — a sticky header alongside the row — but the copies toggle together,
// so clicking whichever comes first shuts both.
function collapseNextTestFile() {
if (!DIFF_TAB.test(document.location.pathname)) {
tab = null;
return;
}
if (tab !== document.location.pathname) {
tab = document.location.pathname;
attempts = new Map();
}
for (const toggle of document.querySelectorAll(COLLAPSE)) {
const path = filePath(toggle);
if (!path || manual.has(path) || !isTestFile(path)) continue;
const done = attempts.get(path) || 0;
if (done >= MAX_COLLAPSES) continue;
attempts.set(path, done + 1);
toggle.click();
return;
}
}
// A real click on a collapsed file's toggle hands that file over to the reader.
document.addEventListener('click', event => {
if (!event.isTrusted || !event.target.closest) return;
const toggle = event.target.closest('button');
if (!toggle || toggle.getAttribute('aria-label') !== EXPAND) return;
const path = filePath(toggle);
if (path && isTestFile(path)) manual.add(path);
}, true);
// Rows render progressively as the list scrolls, and the Diff tab is reached
// by client-side navigation, so poll instead of running once.
setInterval(collapseNextTestFile, PACE);
collapseNextTestFile();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment