Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/addons/settings/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
@import "../../css/colors.css";
@import "../../css/filters.css";

*{
transition: all 0.2s ease-in-out;
}
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the universal transition: all rule

Applying transition: all 0.2s to * forces every property change (layout, focus outlines, form controls, etc.) on the page to animate. This introduces jank, delays focus indicators (hurting accessibility), and overrides carefully tuned transitions defined later in the file. Please drop the universal rule and scope the animation to the specific elements you want to animate.

-*{
-    transition: all 0.2s ease-in-out;
-}
+.addon-group-expand-container {
+    transition: transform 0.2s ease-in-out, background 0.2s ease-in-out;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
*{
transition: all 0.2s ease-in-out;
}
.addon-group-expand-container {
transition: transform 0.2s ease-in-out, background 0.2s ease-in-out;
}
🤖 Prompt for AI Agents
In src/addons/settings/settings.css around lines 20 to 22, remove the universal
rule "transition: all 0.2s ease-in-out" and instead apply scoped transitions
only to the elements and properties that should animate (for example buttons,
links, input controls, icons, and containers) using explicit properties like
transition: background-color 0.2s, color 0.2s, opacity 0.2s, transform 0.2s;
avoid animating layout or focus outlines and ensure focus states are
instantaneous or have separate, accessible timing to prevent delayed focus
indicators.


body {
background-color: $page-background;
color: $page-foreground;
Expand Down Expand Up @@ -190,6 +194,7 @@ a:active, a:focus {
}
.addon-group-name:hover .addon-group-expand-container {
background: $ui-black-transparent;
transform: scale(1.1);
}
.addon-group-expand-icon {
width: 100%;
Expand Down
266 changes: 266 additions & 0 deletions test/unit/dependencies/package-lock.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/* eslint-env jest */
/**
* Tests for package-lock.json
*
* This test suite validates the structure and integrity of the package lock file,
* ensuring dependencies are properly locked and secure.
*/
const fs = require('fs');
const path = require('path');

describe('Package Lock File', () => {
let packageLock;
let packageLockPath;

beforeAll(() => {
packageLockPath = path.join(__dirname, '../../../package-lock.json');
const content = fs.readFileSync(packageLockPath, 'utf8');
packageLock = JSON.parse(content);
});

describe('JSON Structure', () => {
test('package-lock.json is valid JSON', () => {
expect(packageLock).toBeDefined();
expect(typeof packageLock).toBe('object');
});

test('has required top-level fields', () => {
expect(packageLock).toHaveProperty('name');
expect(packageLock).toHaveProperty('version');
expect(packageLock).toHaveProperty('lockfileVersion');
});

test('lockfile version is supported', () => {
const version = packageLock.lockfileVersion;
expect([1, 2, 3]).toContain(version);
});

test('has packages section', () => {
expect(packageLock).toHaveProperty('packages');
expect(typeof packageLock.packages).toBe('object');
});
});

describe('Dependency Updates', () => {
test('scratch-blocks dependency is updated', () => {
const scratchBlocks = Object.entries(packageLock.packages || {})
.find(([key]) => key.includes('scratch-blocks'));

expect(scratchBlocks).toBeDefined();
});

test('scratch-vm dependency is updated', () => {
const scratchVm = Object.entries(packageLock.packages || {})
.find(([key]) => key.includes('scratch-vm'));

expect(scratchVm).toBeDefined();
});

test('git dependencies have valid commit hashes', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.resolved && pkg.resolved.startsWith('git+ssh://')) {
const match = pkg.resolved.match(/#([a-f0-9]{40})/);
expect(match).toBeTruthy();
expect(match[1].length).toBe(40);
}
});
});

test('git dependencies use SSH protocol', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.resolved && pkg.resolved.includes('github.com')) {

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High test

'
github.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

Copilot Autofix

AI 4 months ago

To address the issue, the check for 'github.com' in the URL should not be performed with .includes(), but rather by robustly parsing the URL and checking its host component. Specifically, the correct way is to use the built-in Node.js URL class to parse pkg.resolved and check if the .host or .hostname is exactly 'github.com' or, if subdomains are allowed, ends with '.github.com' (after a dot or at the start). In this context, the goal is probably to ensure the dependency is from GitHub proper, not from any arbitrary domain containing that string.

So, in the test on line 71, instead of testing pkg.resolved.includes('github.com'), parse pkg.resolved with new URL(), extract the hostname, and check if it equals 'github.com' (and/or add other allowed forms as needed).

This requires:

  • Importing the URL class if necessary (in Node.js, it's global and does not need an import).
  • Replacing the substring check with robust property access on the parsed URL object.
Suggested changeset 1
test/unit/dependencies/package-lock.test.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/unit/dependencies/package-lock.test.js b/test/unit/dependencies/package-lock.test.js
--- a/test/unit/dependencies/package-lock.test.js
+++ b/test/unit/dependencies/package-lock.test.js
@@ -68,9 +68,16 @@
 
         test('git dependencies use SSH protocol', () => {
             Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
-                if (pkg.resolved && pkg.resolved.includes('github.com')) {
-                    if (pkg.resolved.startsWith('git+')) {
-                        expect(pkg.resolved).toContain('git+ssh://');
+                if (pkg.resolved) {
+                    try {
+                        const resolvedUrl = new URL(pkg.resolved.replace(/^git\+/, ''));
+                        if (resolvedUrl.hostname === 'github.com') {
+                            if (pkg.resolved.startsWith('git+')) {
+                                expect(pkg.resolved).toContain('git+ssh://');
+                            }
+                        }
+                    } catch (e) {
+                        // ignore invalid URLs in resolved (they'll fail other tests)
                     }
                 }
             });
EOF
@@ -68,9 +68,16 @@

test('git dependencies use SSH protocol', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.resolved && pkg.resolved.includes('github.com')) {
if (pkg.resolved.startsWith('git+')) {
expect(pkg.resolved).toContain('git+ssh://');
if (pkg.resolved) {
try {
const resolvedUrl = new URL(pkg.resolved.replace(/^git\+/, ''));
if (resolvedUrl.hostname === 'github.com') {
if (pkg.resolved.startsWith('git+')) {
expect(pkg.resolved).toContain('git+ssh://');
}
}
} catch (e) {
// ignore invalid URLs in resolved (they'll fail other tests)
}
}
});
Copilot is powered by AI and may make mistakes. Always verify output.
if (pkg.resolved.startsWith('git+')) {
expect(pkg.resolved).toContain('git+ssh://');
}
}
});
});
});

describe('Integrity Checks', () => {
test('all packages have integrity hashes', () => {
const packages = Object.entries(packageLock.packages || {});
const packagesWithResolved = packages.filter(([, pkg]) => pkg.resolved);

packagesWithResolved.forEach(([key, pkg]) => {
if (!pkg.resolved.startsWith('git+')) {
expect(pkg).toHaveProperty('integrity');
}
});
});

test('integrity hashes use strong algorithms', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.integrity) {
expect(pkg.integrity).toMatch(/^sha(256|512)-/);
}
});
});

test('git dependencies have integrity hashes', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.resolved && pkg.resolved.startsWith('git+ssh://')) {
expect(pkg.integrity).toBeDefined();
expect(typeof pkg.integrity).toBe('string');
}
});
});
});

describe('Version Consistency', () => {
test('root package version matches', () => {
const packageJsonPath = path.join(__dirname, '../../../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));

expect(packageLock.version).toBe(packageJson.version);
});

test('all dependencies have valid semantic versions', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.version && !pkg.resolved?.startsWith('git+')) {
expect(pkg.version).toMatch(/^\d+\.\d+\.\d+/);
}
});
});
});

describe('Security Considerations', () => {
test('no obvious security vulnerabilities in dependency versions', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.name && pkg.version) {
const match = pkg.version.match(/^(\d+)/);
if (match) {
const majorVersion = parseInt(match[1]);
if (key.includes('scratch-')) {
expect(majorVersion).toBeGreaterThanOrEqual(0);
}
}
}
});
});

test('dependencies are from trusted sources', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.resolved && pkg.resolved.startsWith('http')) {
expect(pkg.resolved).toMatch(/npmjs\.org|github\.com/);
}
});
});

test('no suspicious package names', () => {
Object.entries(packageLock.packages || {}).forEach(([key, pkg]) => {
if (pkg.name) {
expect(pkg.name).not.toMatch(/^(j5|reaqt|nodej5)/);
}
});
});
});

describe('Dependency Graph', () => {
test('no circular dependencies in immediate deps', () => {
const visited = new Set();
const checkCircular = (pkgKey, path = []) => {
if (path.includes(pkgKey)) {
return false;
}
if (visited.has(pkgKey)) {
return true;
}
visited.add(pkgKey);
return true;
};

Object.keys(packageLock.packages || {}).forEach(key => {
expect(checkCircular(key)).toBe(true);
});
});

test('root package has dependencies defined', () => {
const rootPackage = packageLock.packages[''];
expect(rootPackage).toBeDefined();
expect(rootPackage.dependencies || rootPackage.devDependencies).toBeDefined();
});
});

describe('File Integrity', () => {
test('file is properly formatted JSON', () => {
const raw = fs.readFileSync(packageLockPath, 'utf8');
expect(() => JSON.parse(raw)).not.toThrow();
});

test('file uses consistent indentation', () => {
const raw = fs.readFileSync(packageLockPath, 'utf8');
const lines = raw.split('\n');
const indentedLines = lines.filter(line => line.startsWith(' '));

if (indentedLines.length > 0) {
const firstIndent = indentedLines[0].match(/^ +/);
if (firstIndent) {
const indentSize = firstIndent[0].length;
expect(indentSize % 2).toBe(0);
}
}
});

test('file ends with newline', () => {
const raw = fs.readFileSync(packageLockPath, 'utf8');
expect(raw.endsWith('\n')).toBe(true);
});
});

describe('Specific Package Updates', () => {
test('scratch-blocks points to correct commit', () => {
const scratchBlocks = Object.entries(packageLock.packages || {})
.find(([key]) => key.includes('scratch-blocks'));

if (scratchBlocks) {
const [, pkg] = scratchBlocks;
if (pkg.resolved) {
expect(pkg.resolved).toContain('7b24920ea6fc99228b63ef7ada5091e19c4f4553');
}
}
});

test('scratch-vm points to correct commit', () => {
const scratchVm = Object.entries(packageLock.packages || {})
.find(([key]) => key.includes('scratch-vm'));

if (scratchVm) {
const [, pkg] = scratchVm;
if (pkg.resolved) {
expect(pkg.resolved).toContain('279ea2a18b244bedd545b59a00247df9a841cf9d');
}
}
});

test('updated packages maintain license information', () => {
['scratch-blocks', 'scratch-vm'].forEach(pkgName => {
const pkg = Object.entries(packageLock.packages || {})
.find(([key]) => key.includes(pkgName));

if (pkg) {
const [, pkgData] = pkg;
expect(pkgData.license).toBeDefined();
expect(typeof pkgData.license).toBe('string');
}
});
});
});

describe('Regression Tests', () => {
test('maintains backward compatibility with package structure', () => {
const essential = ['webpack', 'babel', 'react'];
essential.forEach(pkgPrefix => {
const found = Object.keys(packageLock.packages || {})
.some(key => key.includes(pkgPrefix));
expect(found).toBe(true);
});
});

test('no unexpected package removals', () => {
const scratchPackages = Object.keys(packageLock.packages || {})
.filter(key => key.includes('scratch-'));
expect(scratchPackages.length).toBeGreaterThan(0);
});
});
});
Loading