Commit 2651071d authored by Enrique Alcántara's avatar Enrique Alcántara 3️⃣ Committed by Tristan Read
Browse files

fix: Re-create workflow terminal when closed

parent 0c03c230
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -68,6 +68,7 @@ module.exports = {
    onDidChangeActiveColorTheme: jest.fn(),
    onDidChangeTerminalShellIntegration: jest.fn(),
    onDidEndTerminalShellExecution: jest.fn(),
    onDidCloseTerminal: jest.fn(),
    createTerminal: jest.fn(),
    createWebviewPanel: jest.fn(),
    showTextDocument: jest.fn(),
+63 −9
Original line number Diff line number Diff line
@@ -58,15 +58,23 @@ describe('Terminal Manager', () => {
        expect.any(Function),
      );
    });

    it('listens to onDidCloseTerminal event', () => {
      terminalManager.setupRequests(mockClient);

      expect(window.onDidCloseTerminal).toHaveBeenCalledWith(expect.any(Function));
    });
  });

  describe('dispose', () => {
    let eventFn: RunCommandEventHandler;
    let shellExecutionFn: (e: TerminalShellExecutionEndEvent) => Disposable;
    let listenerDispose: jest.Mock;
    let closeTerminalDispose: jest.Mock;

    beforeEach(async () => {
      listenerDispose = jest.fn();
      closeTerminalDispose = jest.fn();
      jest.mocked(mockClient.onRequest).mockImplementation((event: string, fn) => {
        if (event === '$/gitlab/runCommand') {
          eventFn = fn as RunCommandEventHandler;
@@ -77,6 +85,9 @@ describe('Terminal Manager', () => {
        shellExecutionFn = fn;
        return { dispose() {} };
      });
      jest.mocked(window.onDidCloseTerminal).mockImplementationOnce(() => {
        return { dispose: closeTerminalDispose };
      });
      jest.mocked(mockShellIntegration.executeCommand).mockReturnValue(mockExecution);

      terminalManager.setupRequests(mockClient);
@@ -108,11 +119,15 @@ describe('Terminal Manager', () => {
      terminalManager.dispose();

      expect(listenerDispose).toHaveBeenCalled();
      expect(closeTerminalDispose).toHaveBeenCalled();
      expect(mockTerminal.dispose).toHaveBeenCalled();
    });
  });

  describe('$/gitlab/runCommand', () => {
    const workflowId = '1234';
    const command = 'npm';
    const args = ['run', 'test:unit'];
    let eventFn: RunCommandEventHandler;
    let shellIntegrationFn: (
      e: TerminalShellIntegrationChangeEvent,
@@ -120,12 +135,15 @@ describe('Terminal Manager', () => {
      // disposables?: Disposable[],
    ) => Disposable;
    let shellExecutionFn: (e: TerminalShellExecutionEndEvent) => Disposable;
    let onDidCloseTerminalListener: (terminal: Terminal) => void;
    let shellExecDispose: jest.Mock;
    let shellIntDispose: jest.Mock;

    beforeEach(() => {
      shellExecDispose = jest.fn();
      shellIntDispose = jest.fn();
      onDidCloseTerminalListener = jest.fn();

      jest.mocked(mockClient.onRequest).mockImplementation((event: string, fn) => {
        if (event === '$/gitlab/runCommand') {
          eventFn = fn as RunCommandEventHandler;
@@ -141,14 +159,16 @@ describe('Terminal Manager', () => {
        return { dispose: shellExecDispose };
      });
      jest.mocked(mockShellIntegration.executeCommand).mockReturnValue(mockExecution);
      jest.mocked(window.onDidCloseTerminal).mockImplementationOnce(listener => {
        onDidCloseTerminalListener = listener;

        return { dispose() {} };
      });

      terminalManager.setupRequests(mockClient);
    });

    it('executes a command when the shell integration is available', async () => {
      const workflowId = '1234';
      const command = 'npm';
      const args = ['run', 'test:unit'];
      const processOutput = ['running tests...\n', 'all tests passed!'];
      mockShellIntegrationGetter.mockReturnValue(undefined);

@@ -193,9 +213,6 @@ describe('Terminal Manager', () => {
    });

    it('reuses a terminal when the same workflow ID is used', async () => {
      const workflowId = '1234';
      const command = 'npm';
      const args = ['run', 'test:unit'];
      mockShellIntegrationGetter.mockReturnValue(mockShellIntegration);

      jest.mocked(mockExecution.read).mockImplementation(async function* read() {
@@ -231,10 +248,47 @@ describe('Terminal Manager', () => {
      expect(window.createTerminal).toHaveBeenCalledTimes(1);
    });

    it('re-creates terminal if the terminal is closed', async () => {
      mockShellIntegrationGetter.mockReturnValue(mockShellIntegration);

      jest.mocked(mockExecution.read).mockImplementation(async function* read() {
        yield '';
      });

      let result = eventFn({ workflowId, command, args });

      await jest.advanceTimersToNextTimerAsync();

      shellExecutionFn({
        execution: mockExecution,
        exitCode: 0,
        terminal: mockTerminal,
        shellIntegration: mockShellIntegration,
      });

      await result;

      expect(window.createTerminal).toHaveBeenCalledTimes(1);

      onDidCloseTerminalListener(mockTerminal);

      result = eventFn({ workflowId, command, args });

      await jest.advanceTimersToNextTimerAsync();

      shellExecutionFn({
        execution: mockExecution,
        exitCode: 0,
        terminal: mockTerminal,
        shellIntegration: mockShellIntegration,
      });

      await jest.advanceTimersToNextTimerAsync();

      expect(window.createTerminal).toHaveBeenCalledTimes(2);
    });

    it('throws when shell integration is not available', async () => {
      const workflowId = '1234';
      const command = 'npm';
      const args = ['run', 'test:unit'];
      mockShellIntegrationGetter.mockReturnValue(undefined);

      const result = eventFn({ workflowId, command, args });
+27 −12
Original line number Diff line number Diff line
@@ -3,22 +3,32 @@ import { BaseLanguageClient } from 'vscode-languageclient';
import { log } from '../log';

export class TerminalManager implements Disposable {
  #terminals: Record<string, Terminal>;
  #terminals: Map<string, Terminal>;

  #listener: Disposable | undefined;
  #disposables: Disposable[] = [];

  constructor() {
    this.#terminals = {};
    this.#terminals = new Map();
  }

  setupRequests(client: BaseLanguageClient) {
    this.#listener = client.onRequest(
      '$/gitlab/runCommand',
      async ({ workflowId, command, args }) => {
    this.#disposables.push(
      client.onRequest('$/gitlab/runCommand', async ({ workflowId, command, args }) => {
        log.debug(`Running command: ${command} ${args.join(' ')}`);

        return this.#executeCommand(workflowId, command, args);
      },
      }),
    );

    this.#disposables.push(
      window.onDidCloseTerminal(closedTerminal => {
        for (const [workflowId, terminal] of this.#terminals.entries()) {
          if (terminal === closedTerminal) {
            this.#terminals.delete(workflowId);
            break;
          }
        }
      }),
    );
  }

@@ -53,7 +63,7 @@ export class TerminalManager implements Disposable {
  }

  async #getOrCreateTerminal(workflowId: string) {
    return this.#terminals[workflowId] ?? this.#createTerminal(workflowId);
    return this.#terminals.get(workflowId) ?? this.#createTerminal(workflowId);
  }

  async #createTerminal(workflowId: string) {
@@ -62,11 +72,11 @@ export class TerminalManager implements Disposable {
      isTransient: true,
    });

    this.#terminals[workflowId] = terminal;
    this.#terminals.set(workflowId, terminal);

    await this.#listenForShellIntegration(terminal);

    return this.#terminals[workflowId];
    return terminal;
  }

  #listenForShellIntegration(term: Terminal) {
@@ -91,7 +101,12 @@ export class TerminalManager implements Disposable {
  }

  dispose() {
    Object.values(this.#terminals).forEach(term => term.dispose());
    this.#listener?.dispose();
    this.#terminals.forEach(term => {
      term.dispose();
    });

    this.#disposables.forEach(disposable => {
      disposable.dispose();
    });
  }
}