diff --git a/.comfyignore b/.comfyignore new file mode 100644 index 00000000..02f6ebab --- /dev/null +++ b/.comfyignore @@ -0,0 +1,25 @@ +# Left out of the Comfy Registry package (comfy node publish). Git clones still +# have everything. tests/test_package_contents.py holds these rules to what +# ComfyUI needs at run time; keep each one anchored to the root with a leading /. + +# Development folders +/tests/ +/tools/ +/docs/ +/.github/ + +# Screenshots used only by README-legacy.md on GitHub +/images/ + +# Development files +/TESTING.md +/Makefile +/requirements-dev.txt +/.coveragerc +/.flake8 +/.pre-commit-config.yaml +/.gitignore + +# Stray images no code uses +/image_meta_data.png +/test_metadata_file.png diff --git a/TESTING.md b/TESTING.md index 79522c97..948c70b5 100644 --- a/TESTING.md +++ b/TESTING.md @@ -668,6 +668,10 @@ Browser tests: `workspace-stale-source.spec.mjs` (`/__test__/sources/reverse` an `/__test__/scope-key/rotate`, both undone by every reset); the manual-Refresh recovery specs in `workspace-images.spec.mjs` answer every rescope 409. +## Registry package + +`comfy node publish` zips the tracked files minus `.comfyignore`, so a new development file or folder at the root ships until it is listed there. `tests/test_package_contents.py` keeps each rule anchored to the root, checks that development paths are left out and that nothing ComfyUI needs at run time is. Git clones are unaffected. + ## Windows portability preflight Before changing paths, SQLite, streams, restore or cleanup: diff --git a/tests/browser/legacy-removal-notice.spec.mjs b/tests/browser/legacy-removal-notice.spec.mjs new file mode 100644 index 00000000..b0459a52 --- /dev/null +++ b/tests/browser/legacy-removal-notice.spec.mjs @@ -0,0 +1,47 @@ +// #615: each old page says it will be removed and links to where its job lives now. The page keeps working, and the +// notice takes its own room: nothing is pushed off a page whose layout fills the window. +import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs'; +import { test, expect } from './fixtures.mjs'; + +const PAGES = Object.freeze([ + { path: '/prompt_manager/admin', section: 'library', heading: 'Prompt library' }, + { path: '/prompt_manager/gallery', section: 'images', heading: 'Images' }, + { path: '/prompt_manager/gallery.html', section: 'inspect', heading: 'Inspect a file' }, +]); + +test.beforeEach(async ({ page, request, browserName }) => { + await startCoverage(page, browserName); + await expect(await request.post('/__test__/reset', {})).toBeOK(); + await page.addInitScript(() => { + localStorage.setItem('pm_update_notice_dismissed', '3.2.0'); + localStorage.setItem('gallerySettings', JSON.stringify({ checkThumbnailsAtStartup: false })); + }); +}); + +test.afterEach(async ({ page, browserName }) => { + await saveCoverage(page, browserName); +}); + +for (const { path, section, heading } of PAGES) { + test(`${path} says it will be removed and links to ${section}`, async ({ page }) => { + await page.goto(path); + const notice = page.locator('#legacy-removal-notice'); + await expect(notice).toBeVisible(); + await expect(notice).toContainText('This page will be removed in a future release.'); + await notice.getByRole('link', { name: 'Open the workspace' }).click(); + await expect(page).toHaveURL(new RegExp(`/prompt_manager/workspace#/${section}$`)); + await expect(page.locator('#workspace-main h1')).toHaveText(heading); + }); +} + +test('the notice leaves the metadata page filling the window', async ({ page }) => { + await page.goto('/prompt_manager/gallery.html'); + await expect(page.locator('#legacy-removal-notice')).toBeVisible(); + const { bottom, height, scroll } = await page.locator('#image-area').evaluate(area => ({ + bottom: area.getBoundingClientRect().bottom, + height: window.innerHeight, + scroll: document.documentElement.scrollHeight, + })); + expect(bottom).toBeLessThanOrEqual(height); + expect(scroll).toBeLessThanOrEqual(height); +}); diff --git a/tests/test_package_contents.py b/tests/test_package_contents.py new file mode 100644 index 00000000..0de4c60f --- /dev/null +++ b/tests/test_package_contents.py @@ -0,0 +1,104 @@ +"""What the Registry package leaves out (#615). + +``comfy node publish`` zips the tracked files minus ``.comfyignore``. The checks +here hold the two ways that can go wrong: a development folder creeping back +into the download, or a rule that drops something ComfyUI needs at run time. +""" + +import ast +import fnmatch +import re +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_DIRS = ("py", "database", "utils", "web") +DEVELOPMENT = ( + "tests/test_package_contents.py", + "tools/vendor/package.json", + "docs/application-redesign.md", + ".github/workflows/publish.yml", + "images/x.png", + "TESTING.md", + "requirements-dev.txt", + "image_meta_data.png", + "test_metadata_file.png", +) +KEPT = ( + "__init__.py", + "prompt_manager.py", + "pyproject.toml", + "requirements.txt", + "LICENSE", + "README.md", + "README-legacy.md", + "assets/readme/hero.webp", +) + + +def _patterns(): + lines = (ROOT / ".comfyignore").read_text(encoding="utf-8").splitlines() + return [line.strip() for line in lines if line.strip() and line[0] != "#"] + + +def _ignored(path): + """The rules this file allows: ``folder/`` from the root, or a root file.""" + for pattern in _patterns(): + if pattern.endswith("/"): + if path.startswith(pattern.lstrip("/")): + return True + elif fnmatch.fnmatchcase(path, pattern.lstrip("/")): + return True + return False + + +class TestPackageContents(unittest.TestCase): + def test_every_rule_is_anchored_to_the_root(self): + # An unanchored "tests/" would also drop web/js/tests, and "images/" a + # runtime folder of that name deeper down. + for pattern in _patterns(): + self.assertTrue(pattern.startswith("/"), pattern) + self.assertNotIn("!", pattern) + + def test_development_files_are_left_out(self): + for path in DEVELOPMENT: + self.assertTrue(_ignored(path), path) + + def test_what_comfyui_needs_stays(self): + for path in KEPT: + self.assertFalse(_ignored(path), path) + for folder in RUNTIME_DIRS: + for file in (ROOT / folder).rglob("*"): + if file.is_file(): + relative = file.relative_to(ROOT).as_posix() + self.assertFalse(_ignored(relative), relative) + + def test_runtime_code_imports_nothing_left_out(self): + sources = list(ROOT.glob("*.py")) + for folder in ("py", "database", "utils"): + sources.extend((ROOT / folder).rglob("*.py")) + for source in sources: + tree = ast.parse(source.read_text(encoding="utf-8")) + for node in ast.walk(tree): + names = [] + if isinstance(node, ast.Import): + names = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom) and not node.level: + names = [node.module or ""] + for name in names: + self.assertNotIn(name.split(".")[0], ("tests", "tools"), source) + + def test_the_old_pages_name_their_replacement(self): + for page, section in ( + ("admin", "library"), + ("gallery", "images"), + ("metadata", "inspect"), + ): + html = (ROOT / "web" / f"{page}.html").read_text(encoding="utf-8") + self.assertRegex(html, r'id="legacy-removal-notice"') + self.assertRegex(html, re.escape(f'href="workspace#/{section}"')) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/web/admin.html b/web/admin.html index e55fbda6..d3fc586f 100644 --- a/web/admin.html +++ b/web/admin.html @@ -20,6 +20,7 @@
+