Compare commits
	
		
			10 Commits
		
	
	
		
			6359-sub-o
			...
			one-weird-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					850aca546b | ||
| 
						 | 
					22962452e3 | ||
| 
						 | 
					cfdc6d7f20 | ||
| 
						 | 
					bafe8c33a9 | ||
| 
						 | 
					62dcf76798 | ||
| 
						 | 
					04adb790a0 | ||
| 
						 | 
					6f46b4d87e | ||
| 
						 | 
					4fff6b035b | ||
| 
						 | 
					d48e11bbf7 | ||
| 
						 | 
					8cc4eca3e8 | 
@@ -23,7 +23,10 @@ const config = {
 | 
			
		||||
    ignoreHTTPSErrors: true,
 | 
			
		||||
    screenshot: 'off',
 | 
			
		||||
    trace: 'on-first-retry',
 | 
			
		||||
    video: 'off'
 | 
			
		||||
    video: 'off',
 | 
			
		||||
    launchOptions: {
 | 
			
		||||
      args: ['--js-flags=--expose-gc']
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  projects: [
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								e2e/test-data/memory-leak-detection.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								e2e/test-data/memory-leak-detection.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2023, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const filePath = 'e2e/test-data/memory-leak-detection.json';
 | 
			
		||||
/**
 | 
			
		||||
 * Executes tests to verify that views are not leaking memory on navigation away. This sort of
 | 
			
		||||
 * memory leak is generally caused by a failure to clean up registered listeners.
 | 
			
		||||
 *
 | 
			
		||||
 * These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
 | 
			
		||||
 *
 | 
			
		||||
 * In order to modify the test data set:
 | 
			
		||||
 * 1. Run Open MCT locally (npm start)
 | 
			
		||||
 * 2. Right click on a folder in the tree, and select "Import From JSON"
 | 
			
		||||
 * 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
 | 
			
		||||
 * 4. Click "OK"
 | 
			
		||||
 * 5. Modify test objects as desired
 | 
			
		||||
 * 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
 | 
			
		||||
 * 7. Copy the exported file to ../test-data/memory-leak-detection.json
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
test.describe('Navigation memory leak is not detected in', () => {
 | 
			
		||||
  test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
    // Go to baseURL
 | 
			
		||||
    await page.goto('./', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Click a:has-text("My Items")
 | 
			
		||||
    await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
      button: 'right'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Click text=Import from JSON
 | 
			
		||||
    await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
    // Upload memory-leak-detection.json
 | 
			
		||||
    await page.setInputFiles('#fileElem', filePath);
 | 
			
		||||
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('plot view', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg');
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('stacked plot view', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg');
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.only('LAD table view', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg');
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('LAD table set', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg');
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('telemetry table view', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'telemetry-table-single-1hz-swg'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('notebook view', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'notebook-memory-leak-detection-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('display layout of a single SWG alphanumeric', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(page, 'display-layout-single-1hz-swg');
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('display layout of a single SWG plot', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'display-layout-single-overlay-plot'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('example imagery view', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'example-imagery-memory-leak-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('display layout of example imagery views', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'display-layout-images-memory-leak-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'display-layout-simple-telemetry'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('flexible layout with plots of swgs', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'flexible-layout-plots-memory-leak-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('flexible layout of example imagery views', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'flexible-layout-images-memory-leak-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('tabbed view of display layouts and time strips', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'tab-view-simple-memory-leak-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.skip('time strip view of telemetry', async ({ page }) => {
 | 
			
		||||
    const result = await navigateToObjectAndDetectMemoryLeak(
 | 
			
		||||
      page,
 | 
			
		||||
      'time-strip-telemetry-memory-leak-test'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
 | 
			
		||||
    expect(result).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
    // Fill Search input
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
 | 
			
		||||
 | 
			
		||||
    //Search Result Appears and is clicked
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
      page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
 | 
			
		||||
      page.waitForNavigation()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Register a finalization listener on the root node for the view. This tends to be the last thing to be
 | 
			
		||||
    // garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
 | 
			
		||||
    // for detecting memory leaks.
 | 
			
		||||
    await page.evaluate(() => {
 | 
			
		||||
      window.gcPromise = new Promise((resolve) => {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
        window.fr = new FinalizationRegistry(resolve);
 | 
			
		||||
        window.fr.register(
 | 
			
		||||
          window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,
 | 
			
		||||
          'navigatedObject',
 | 
			
		||||
          window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Nav back to folder
 | 
			
		||||
    await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
 | 
			
		||||
    await page.waitForNavigation();
 | 
			
		||||
 | 
			
		||||
    // This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
 | 
			
		||||
    // In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
 | 
			
		||||
    await page.evaluate(() => {
 | 
			
		||||
      const gcPromise = window.gcPromise;
 | 
			
		||||
      window.gcPromise = null;
 | 
			
		||||
 | 
			
		||||
      // Manually invoke the garbage collector once all references are removed.
 | 
			
		||||
      window.gc();
 | 
			
		||||
 | 
			
		||||
      return gcPromise;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Clean up the finalization registry since we don't need it any more.
 | 
			
		||||
    await page.evaluate(() => {
 | 
			
		||||
      window.fr = null;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // If we get here without timing out, it means the garbage collection promise resolved and the test passed.
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
@@ -993,9 +993,6 @@ export default {
 | 
			
		||||
 | 
			
		||||
      this.config.yAxisLabel = this.config.yAxis.get('label');
 | 
			
		||||
 | 
			
		||||
      this.cursorGuideVertical = this.$refs.cursorGuideVertical;
 | 
			
		||||
      this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
 | 
			
		||||
 | 
			
		||||
      this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
 | 
			
		||||
      this.yAxisListWithRange.forEach((yAxis) => {
 | 
			
		||||
        this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
 | 
			
		||||
@@ -1123,8 +1120,8 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    updateCrosshairs(event) {
 | 
			
		||||
      this.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';
 | 
			
		||||
      this.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';
 | 
			
		||||
      this.$refs.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';
 | 
			
		||||
      this.$refs.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    trackChartElementBounds(event) {
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,7 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
 | 
			
		||||
    this.stopListening();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    isEnabledXKeyToggle() {
 | 
			
		||||
 
 | 
			
		||||
@@ -168,6 +168,9 @@ export default {
 | 
			
		||||
    this.loaded = true;
 | 
			
		||||
    this.setUpYAxisOptions();
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.stopListening();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    initAxisAndSeriesConfig() {
 | 
			
		||||
      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import TableComponent from './components/table.vue';
 | 
			
		||||
import TelemetryTable from './TelemetryTable';
 | 
			
		||||
import mount from 'utils/mount';
 | 
			
		||||
import { markRaw } from 'vue';
 | 
			
		||||
 | 
			
		||||
export default class TelemetryTableView {
 | 
			
		||||
  constructor(openmct, domainObject, objectPath) {
 | 
			
		||||
@@ -9,12 +10,6 @@ export default class TelemetryTableView {
 | 
			
		||||
    this.objectPath = objectPath;
 | 
			
		||||
    this._destroy = null;
 | 
			
		||||
    this.component = null;
 | 
			
		||||
 | 
			
		||||
    Object.defineProperty(this, 'table', {
 | 
			
		||||
      value: new TelemetryTable(domainObject, openmct),
 | 
			
		||||
      enumerable: false,
 | 
			
		||||
      configurable: false
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getViewContext() {
 | 
			
		||||
@@ -41,9 +36,14 @@ export default class TelemetryTableView {
 | 
			
		||||
    if (this._destroy) {
 | 
			
		||||
      this._destroy();
 | 
			
		||||
    }
 | 
			
		||||
    delete this.component;
 | 
			
		||||
    delete this._destroy;
 | 
			
		||||
    this.openmct.app._context.optionsCache = new WeakMap();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  show(element, editMode) {
 | 
			
		||||
    const telemetryTable = markRaw(new TelemetryTable(this.domainObject, this.openmct));
 | 
			
		||||
 | 
			
		||||
    const { vNode, destroy } = mount(
 | 
			
		||||
      {
 | 
			
		||||
        el: element,
 | 
			
		||||
@@ -53,7 +53,7 @@ export default class TelemetryTableView {
 | 
			
		||||
        provide: {
 | 
			
		||||
          openmct: this.openmct,
 | 
			
		||||
          objectPath: this.objectPath,
 | 
			
		||||
          table: this.table,
 | 
			
		||||
          table: telemetryTable,
 | 
			
		||||
          currentView: this
 | 
			
		||||
        },
 | 
			
		||||
        data() {
 | 
			
		||||
 
 | 
			
		||||
@@ -145,10 +145,10 @@ export default {
 | 
			
		||||
        let column = new TelemetryTableColumn(this.openmct, metadatum);
 | 
			
		||||
        this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);
 | 
			
		||||
        // if units are available, need to add columns to be hidden
 | 
			
		||||
        if (metadatum.unit !== undefined) {
 | 
			
		||||
          let unitColumn = new TelemetryTableUnitColumn(this.openmct, metadatum);
 | 
			
		||||
          this.tableConfiguration.addSingleColumnForObject(telemetryObject, unitColumn);
 | 
			
		||||
        }
 | 
			
		||||
        // if (metadatum.unit !== undefined) {
 | 
			
		||||
        //   let unitColumn = new TelemetryTableUnitColumn(this.openmct, metadatum);
 | 
			
		||||
        //   this.tableConfiguration.addSingleColumnForObject(telemetryObject, unitColumn);
 | 
			
		||||
        // }
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    toggleHeaderVisibility() {
 | 
			
		||||
 
 | 
			
		||||
@@ -291,13 +291,13 @@ const AUTO_SCROLL_TRIGGER_HEIGHT = 100;
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    TelemetryTableRow,
 | 
			
		||||
    TableColumnHeader,
 | 
			
		||||
    search,
 | 
			
		||||
    TableFooterIndicator,
 | 
			
		||||
    ToggleSwitch,
 | 
			
		||||
    SizingRow,
 | 
			
		||||
    ProgressBar
 | 
			
		||||
    TelemetryTableRow: Object.assign({}, TelemetryTableRow),
 | 
			
		||||
    TableColumnHeader: Object.assign({}, TableColumnHeader),
 | 
			
		||||
    search: Object.assign({}, search),
 | 
			
		||||
    TableFooterIndicator: Object.assign({}, TableFooterIndicator),
 | 
			
		||||
    ToggleSwitch: Object.assign({}, ToggleSwitch),
 | 
			
		||||
    SizingRow: Object.assign({}, SizingRow),
 | 
			
		||||
    ProgressBar: Object.assign({}, ProgressBar)
 | 
			
		||||
  },
 | 
			
		||||
  inject: ['openmct', 'objectPath', 'table', 'currentView'],
 | 
			
		||||
  props: {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,8 @@ export default {
 | 
			
		||||
      Object.assign(rawOldObject, rawNewObject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.unmountObservers = [];
 | 
			
		||||
 | 
			
		||||
    this.objectPath.forEach((object) => {
 | 
			
		||||
      if (object) {
 | 
			
		||||
        const unobserve = this.openmct.objects.observe(
 | 
			
		||||
@@ -34,12 +36,15 @@ export default {
 | 
			
		||||
          '*',
 | 
			
		||||
          updateObject.bind(this, object)
 | 
			
		||||
        );
 | 
			
		||||
        this.$once('hook:unmounted', unobserve);
 | 
			
		||||
        this.unmountObservers.push(unobserve);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.$refs.root.removeEventListener('contextMenu', this.showContextMenu);
 | 
			
		||||
    this.unmountObservers.forEach((unobserve) => unobserve());
 | 
			
		||||
 | 
			
		||||
    delete this.unmountObservers;
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    showContextMenu(event) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,17 @@ import { h, render } from 'vue';
 | 
			
		||||
export default function mount(component, { props, children, element, app } = {}) {
 | 
			
		||||
  let el = element;
 | 
			
		||||
 | 
			
		||||
  if (component.components !== undefined) {
 | 
			
		||||
    component.components = Object.keys(component.components).reduce(
 | 
			
		||||
      (componentMap, componentKey) => {
 | 
			
		||||
        componentMap[componentKey] = Object.assign({}, component.components[componentKey]);
 | 
			
		||||
 | 
			
		||||
        return componentMap;
 | 
			
		||||
      },
 | 
			
		||||
      {}
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let vNode = h(component, props, children);
 | 
			
		||||
  if (app && app._context) {
 | 
			
		||||
    vNode.appContext = app._context;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user