UNPKG

@shopify/theme-language-server-common

Version:

<h1 align="center" style="position: relative;" > <br> <img src="https://github.com/Shopify/theme-check-vscode/blob/main/images/shopify_glyph.png?raw=true" alt="logo" width="141" height="160"> <br> Theme Language Server </h1>

394 lines (342 loc) 11.8 kB
import { allChecks } from '@shopify/theme-check-common'; import { MockFileSystem, MockTheme } from '@shopify/theme-check-common/dist/test'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DidChangeConfigurationNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidRenameFilesNotification, PublishDiagnosticsNotification, } from 'vscode-languageserver'; import { MockConnection, mockConnection } from '../test/MockConnection'; import { Dependencies } from '../types'; import { CHECK_ON_CHANGE, CHECK_ON_OPEN, CHECK_ON_SAVE } from './Configuration'; import { startServer } from './startServer'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe('Module: server', () => { const filePath = 'snippets/code.liquid'; const fileURI = `browser:/${filePath}`; const fileContents = `{% render 'foo' %}`; let checkOnChange: boolean | null = null; let checkOnSave: boolean | null = null; let checkOnOpen: boolean | null = null; let connection: MockConnection; let dependencies: ReturnType<typeof getDependencies>; let fileTree: MockTheme; let logger: any; beforeEach(() => { checkOnChange = checkOnSave = checkOnOpen = null; // Initialize all ze mocks... connection = mockConnection(); // Mock answer to workspace/configuration requests connection.spies.sendRequest.mockImplementation(async (method: any, params: any) => { if (method === 'workspace/configuration') { return params.items.map(({ section }: any) => { switch (section) { case CHECK_ON_CHANGE: return checkOnChange; case CHECK_ON_OPEN: return checkOnOpen; case CHECK_ON_SAVE: return checkOnSave; default: return null; } }); } else if (method === 'client/registerCapability') { return null; } else { throw new Error( `Does not know how to mock response to '${method}' requests. Check your test.`, ); } }); fileTree = { 'snippets/code.liquid': fileContents }; logger = vi.fn(); dependencies = getDependencies(logger, fileTree); // Start the server startServer(connection, dependencies); // Stop the time vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("should log Let's roll! on successful setup", async () => { connection.setup(); await flushAsync(); expect(logger).toHaveBeenCalledWith("[SERVER] Let's roll!"); }); it('should debounce calls to runChecks', async () => { connection.setup(); await flushAsync(); connection.openDocument(filePath, `{% echo 'hello' %}`); connection.changeDocument(filePath, `{% echo 'hello w' %}`, 1); connection.changeDocument(filePath, `{% echo 'hello wor' %}`, 2); connection.changeDocument(filePath, `{% echo 'hello world' %}`, 3); await flushAsync(); // Make sure nothing was sent expect(connection.spies.sendNotification).not.toHaveBeenCalled(); // Advance time by debounce time await advanceAndFlush(100); // Make sure you get the diagnostics you'd expect (for the right // version of the file) expect(connection.spies.sendNotification).toHaveBeenCalledOnce(); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { diagnostics: [], uri: fileURI, version: 3, }, ); }); it('should not call runChecks on open, change or save if the configurations are false', async () => { connection.setup( {}, { 'themeCheck.checkOnOpen': false, 'themeCheck.checkOnChange': false, 'themeCheck.checkOnSave': false, }, ); await flushAsync(); connection.openDocument(filePath, fileContents); connection.changeDocument(filePath, fileContents, 1); connection.saveDocument(filePath); await flushAsync(); // run the config check await advanceAndFlush(100); // advance by debounce time // Make sure it cleared expect(connection.spies.sendNotification).toHaveBeenCalled(); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { uri: fileURI, version: undefined, // < this is how we assert that it was cleared diagnostics: [], // < empty array for clear }, ); }); it('should react to configuration changes', async () => { connection.setup( { workspace: { configuration: true, didChangeConfiguration: { dynamicRegistration: true, }, }, }, { 'themeCheck.checkOnOpen': false, 'themeCheck.checkOnChange': false, 'themeCheck.checkOnSave': false, }, ); await flushAsync(); checkOnChange = true; // Invalidate cache connection.triggerNotification(DidChangeConfigurationNotification.type, { settings: null }); await flushAsync(); // Those don't count! connection.spies.sendNotification.mockClear(); // Those weren't changed connection.openDocument(filePath, `{% echo 'hello' %}`); connection.saveDocument(filePath); await flushAsync(); // run the config check await advanceAndFlush(100); // advance by debounce time // Make sure it wasn't called expect(connection.spies.sendNotification).not.toHaveBeenCalled(); connection.changeDocument(filePath, fileContents, 1); await flushAsync(); // run the config check await advanceAndFlush(100); // advance by debounce time expect(connection.spies.sendNotification).toHaveBeenCalled(); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { uri: fileURI, version: 1, diagnostics: [missingTemplateDiagnostic()], }, ); }); it('should trigger a re-check on did create files notifications', async () => { connection.setup(); await flushAsync(); // Setup & expectations connection.openDocument(filePath, fileContents); await flushAsync(); // we need to flush the configuration check await advanceAndFlush(100); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { uri: fileURI, version: 0, diagnostics: [missingTemplateDiagnostic()], }, ); // Clear mocks for future use connection.spies.sendNotification.mockClear(); // Update mock FS with new existing files fileTree['snippets/foo.liquid'] = '...'; fileTree['snippets/bar.liquid'] = '...'; // Trigger create files notification & update mocks connection.triggerNotification(DidCreateFilesNotification.type, { files: [ { uri: 'browser:/snippets/foo.liquid', }, { uri: 'browser:/snippets/bar.liquid', }, ], }); await advanceAndFlush(100); // Verify that we re-check'ed filePath to remove the linting error expect(connection.spies.sendNotification).toHaveBeenCalledOnce(); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { diagnostics: [], uri: fileURI, version: 0, }, ); }); it('should trigger a re-check on did file rename notifications', async () => { connection.setup(); await flushAsync(); // Setup & expectations fileTree['snippets/bar.liquid'] = '...'; connection.openDocument(filePath, fileContents); await flushAsync(); // we need to flush the configuration check await advanceAndFlush(100); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { uri: fileURI, version: 0, diagnostics: [missingTemplateDiagnostic()], }, ); // Reset mocks for different expectations later connection.spies.sendNotification.mockClear(); // Trigger a file rename notification connection.triggerNotification(DidRenameFilesNotification.type, { files: [ { oldUri: 'browser:/snippets/bar.liquid', newUri: 'browser:/snippets/foo.liquid', }, ], }); // Adjust mocks delete fileTree['snippets/bar.liquid']; fileTree['snippets/foo.liquid'] = '...'; // Advance time await advanceAndFlush(100); // Make sure only one publishDiagnostics has been called and that the // error disappears because of the file rename. expect(connection.spies.sendNotification).toHaveBeenCalledOnce(); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { diagnostics: [], uri: fileURI, version: 0, }, ); }); it('should trigger a re-check on did delete files notifications', async () => { connection.setup(); await flushAsync(); // Setup and expectations (no errors) fileTree['snippets/foo.liquid'] = '...'; connection.openDocument(filePath, fileContents); await flushAsync(); // we need to flush the configuration check await advanceAndFlush(100); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { uri: fileURI, version: 0, diagnostics: [], }, ); // Clear mocks for future expectations connection.spies.sendNotification.mockClear(); // Notify about file delete connection.triggerNotification(DidDeleteFilesNotification.type, { files: [ { uri: 'browser:/snippets/foo.liquid', }, ], }); delete fileTree['snippets/foo.liquid']; await advanceAndFlush(100); // Make sure there's an error now that the file no longer exists expect(connection.spies.sendNotification).toHaveBeenCalledOnce(); expect(connection.spies.sendNotification).toHaveBeenCalledWith( PublishDiagnosticsNotification.type, { diagnostics: [missingTemplateDiagnostic()], uri: fileURI, version: 0, }, ); }); // When you're using fake timers and stuff runs async, you want to flush // the async stuff that would happen on a timer. // // We can't simply `await sleep(1)` because the timer is stopped, so we // do this Promise.all thing here that does both. function flushAsync() { return Promise.all([vi.advanceTimersByTimeAsync(1), sleep(1)]); } function advanceAndFlush(ms: number) { vi.advanceTimersByTime(ms); return flushAsync(); } function getDependencies(logger: any, fileTree: MockTheme): Dependencies { const MissingTemplate = allChecks.filter((c) => c.meta.code === 'MissingTemplate'); return { fs: new MockFileSystem(fileTree, 'browser:/'), log: logger, loadConfig: async () => ({ context: 'theme', settings: {}, checks: MissingTemplate, rootUri: 'browser:/', }), themeDocset: { filters: async () => [], objects: async () => [], tags: async () => [], systemTranslations: async () => ({}), }, jsonValidationSet: { schemas: async () => [], }, }; } function missingTemplateDiagnostic() { return { code: 'MissingTemplate', codeDescription: { href: expect.any(String) }, message: "'snippets/foo.liquid' does not exist", severity: 1, source: 'theme-check', range: { start: { character: 10, line: 0, }, end: { character: 15, line: 0, }, }, }; } });