mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-11-01 02:31:46 +08:00
Land the v7 three-terminal UX decision: drop the width/pointer-based 44px touch enlargement so one 32px density holds across desktop, tablet, and mobile, and rebuild entity pages on a responsive record list (ResponsiveRecordList + EntityMobileCard + per-entity mobile summaries). Tighten the nginx security headers (CSP frame-ancestors 'none', X-Frame-Options DENY) to match the static e2e server. Complete the dc3-web audit remediation, including the remaining LOW items from docs/dc3-web-low-defects-handoff.md: streaming-safe message loads in the agentic store, mock menu reparent cycle rejection, paged-list page clamping, fetch/axios mock request normalization, per-table attribute catalogs, post-flush alarm watchers, rowKey-agnostic delete state, localized card text fallbacks, and e2e-server port fail-fast. Gate: vue-tsc, eslint, vitest 687 passed, responsive e2e 64 passed. Co-Authored-By: Claude Code <noreply@anthropic.com>
188 lines
8.3 KiB
TypeScript
188 lines
8.3 KiB
TypeScript
/*
|
|
* Copyright 2016-present the IoT DC3 original author or authors.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {type ComponentOptions, defineComponent, h} from 'vue';
|
|
import {vi} from 'vitest';
|
|
|
|
/**
|
|
* Shared Element Plus stub library for component tests.
|
|
*
|
|
* Centralised so each component test no longer duplicates 30+ lines of
|
|
* inline-template stubs. The stubs intentionally only model the subset
|
|
* of behaviour that the component contracts care about (event names,
|
|
* exposed methods, a couple of props). They are NOT visual fidelity
|
|
* stand-ins.
|
|
*/
|
|
|
|
export const createElButtonStub = () =>
|
|
defineComponent({
|
|
name: 'ElButton',
|
|
props: ['icon', 'plain', 'type', 'circle', 'disabled'],
|
|
emits: ['click'],
|
|
setup(props, {emit, slots}) {
|
|
return () =>
|
|
h(
|
|
'button',
|
|
{
|
|
type: 'button',
|
|
class: ['el-button-stub', props.circle ? 'is-circle' : '', props.type ? `is-${props.type}` : ''],
|
|
'data-icon': typeof props.icon === 'object' && props.icon ? (props.icon as { name?: string }).name : '',
|
|
disabled: props.disabled,
|
|
onClick: (event: MouseEvent) => emit('click', event),
|
|
},
|
|
slots.default?.()
|
|
);
|
|
},
|
|
});
|
|
|
|
export const createElPaginationStub = () =>
|
|
defineComponent({
|
|
name: 'ElPagination',
|
|
props: ['currentPage', 'pageSize', 'pageSizes', 'total'],
|
|
emits: ['size-change', 'current-change'],
|
|
setup(props, {emit}) {
|
|
return () =>
|
|
h('div', {class: 'el-pagination-stub'}, [
|
|
h('span', `${props.currentPage}/${props.pageSize}/${props.total}`),
|
|
h('button', {type: 'button', onClick: () => emit('size-change', 24)}, 'size'),
|
|
h('button', {type: 'button', onClick: () => emit('current-change', 3)}, 'page'),
|
|
]);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* ElForm stub with mockable validate/resetFields.
|
|
* Returns the spies alongside the component so tests can assert on them.
|
|
*/
|
|
export const createElFormStub = () => {
|
|
const validate = vi.fn(() => Promise.resolve(true));
|
|
const resetFields = vi.fn();
|
|
|
|
const ElForm = defineComponent({
|
|
name: 'ElForm',
|
|
props: ['model', 'rules', 'inline'],
|
|
setup(_, {expose, slots}) {
|
|
expose({validate, resetFields});
|
|
return () => h('form', {class: 'el-form-stub'}, slots.default?.());
|
|
},
|
|
});
|
|
|
|
return {ElForm, validate, resetFields};
|
|
};
|
|
|
|
const passthrough = (tag: string, className: string): ComponentOptions => ({
|
|
template: `<${tag} class="${className}"><slot /></${tag}>`,
|
|
});
|
|
|
|
/**
|
|
* Bag of layout/decorator stubs that have no behaviour worth modeling.
|
|
* Use as `stubs: { ...layoutStubs }`.
|
|
*/
|
|
export const layoutStubs: Record<string, ComponentOptions> = {
|
|
// ElCard exposes a named `header` slot in real Element Plus; the
|
|
// passthrough form would silently drop it. Render both header and
|
|
// default slots so cards using `<template #header>` (DashboardCard,
|
|
// TitleCard) keep their header markup discoverable in tests.
|
|
ElCard: {
|
|
template:
|
|
'<section class="el-card-stub"><div class="el-card__header-stub" v-if="$slots.header"><slot name="header" /></div><div class="el-card__body-stub"><slot /></div></section>',
|
|
},
|
|
ElFormItem: passthrough('div', 'el-form-item-stub'),
|
|
ElTooltip: passthrough('span', 'el-tooltip-stub'),
|
|
ElCol: passthrough('div', 'el-col-stub'),
|
|
ElRow: passthrough('div', 'el-row-stub'),
|
|
ElDescriptions: passthrough('section', 'el-descriptions-stub'),
|
|
ElDescriptionsItem: passthrough('div', 'el-descriptions-item-stub'),
|
|
ElDialog: {template: '<section class="el-dialog-stub"><slot /><slot name="footer" /></section>'},
|
|
ElDrawer: passthrough('section', 'el-drawer-stub'),
|
|
ElEmpty: {template: '<div class="el-empty-stub" />'},
|
|
ElSkeleton: {template: '<div class="el-skeleton-stub" />'},
|
|
ElPopconfirm: {template: '<span class="el-popconfirm-stub"><slot name="reference" /></span>'},
|
|
ElTable: passthrough('section', 'el-table-stub'),
|
|
ElTableColumn: {template: '<span class="el-table-column-stub" />'},
|
|
ElTag: passthrough('span', 'el-tag-stub'),
|
|
ElInput: {
|
|
props: ['modelValue', 'disabled', 'readonly'],
|
|
emits: ['update:modelValue'],
|
|
template:
|
|
'<span class="el-input-stub"><input :value="modelValue" :disabled="disabled" :readonly="readonly" @input="$emit(\'update:modelValue\', $event.target.value)" /><span class="el-input-group__append"><slot name="append" /></span></span>',
|
|
},
|
|
ElInputNumber: {template: '<input class="el-input-number-stub" />'},
|
|
ElOption: {template: '<option class="el-option-stub" />'},
|
|
ElSelect: {template: '<select class="el-select-stub"><slot /></select>'},
|
|
ElSegmented: {template: '<div class="el-segmented-stub"><slot /></div>'},
|
|
ElCheckbox: {template: '<input type="checkbox" class="el-checkbox-stub" />'},
|
|
ElSwitch: {template: '<input type="checkbox" class="el-switch-stub" />'},
|
|
ElRadioGroup: passthrough('div', 'el-radio-group-stub'),
|
|
ElRadio: {template: '<input type="radio" class="el-radio-stub" />'},
|
|
ElDivider: {template: '<hr class="el-divider-stub" />'},
|
|
ElLink: passthrough('a', 'el-link-stub'),
|
|
ElDatePicker: {template: '<input class="el-date-picker-stub" />'},
|
|
ElTimePicker: {template: '<input class="el-time-picker-stub" />'},
|
|
ElResult: {
|
|
props: ['title', 'subTitle', 'icon'],
|
|
template:
|
|
'<section class="el-result-stub" :data-icon="icon" :data-title="title" :data-sub-title="subTitle"><slot /><slot name="extra" /></section>',
|
|
},
|
|
ElAlert: {
|
|
props: ['title', 'description'],
|
|
template:
|
|
'<div class="el-alert-stub" :title="title" :data-description="description"><strong v-if="title">{{ title }}</strong><span v-if="description">{{ description }}</span><slot /></div>',
|
|
},
|
|
ElSteps: passthrough('div', 'el-steps-stub'),
|
|
ElStep: passthrough('div', 'el-step-stub'),
|
|
ElBadge: passthrough('span', 'el-badge-stub'),
|
|
ElMenu: passthrough('nav', 'el-menu-stub'),
|
|
ElMenuItem: passthrough('a', 'el-menu-item-stub'),
|
|
ElSubMenu: passthrough('div', 'el-sub-menu-stub'),
|
|
// ElDropdown keeps its menu content in a named `dropdown` slot (the real
|
|
// component teleports it to a popper). Render both slots so layouts can
|
|
// assert trigger and menu content.
|
|
ElDropdown: {
|
|
template:
|
|
'<div class="el-dropdown-stub"><slot /><slot name="dropdown" /></div>',
|
|
},
|
|
ElDropdownMenu: passthrough('div', 'el-dropdown-menu-stub'),
|
|
ElDropdownItem: passthrough('div', 'el-dropdown-item-stub'),
|
|
ElIcon: passthrough('span', 'el-icon-stub'),
|
|
ElAvatar: passthrough('span', 'el-avatar-stub'),
|
|
ElScrollbar: passthrough('div', 'el-scrollbar-stub'),
|
|
ElBreadcrumb: passthrough('nav', 'el-breadcrumb-stub'),
|
|
ElBreadcrumbItem: passthrough('span', 'el-breadcrumb-item-stub'),
|
|
ElContainer: passthrough('div', 'el-container-stub'),
|
|
ElHeader: passthrough('header', 'el-header-stub'),
|
|
ElAside: passthrough('aside', 'el-aside-stub'),
|
|
ElMain: passthrough('main', 'el-main-stub'),
|
|
ElLoading: passthrough('div', 'el-loading-stub'),
|
|
ElTreeSelect: passthrough('div', 'el-tree-select-stub'),
|
|
ElTree: passthrough('div', 'el-tree-stub'),
|
|
ElColorPicker: {template: '<input class="el-color-picker-stub" />'},
|
|
ElTabs: passthrough('div', 'el-tabs-stub'),
|
|
ElTabPane: passthrough('div', 'el-tab-pane-stub'),
|
|
ElCollapse: passthrough('div', 'el-collapse-stub'),
|
|
ElCollapseItem: passthrough('div', 'el-collapse-item-stub'),
|
|
ElCheckboxGroup: passthrough('div', 'el-checkbox-group-stub'),
|
|
ElTransfer: passthrough('div', 'el-transfer-stub'),
|
|
ElUpload: passthrough('div', 'el-upload-stub'),
|
|
ElPopover: passthrough('div', 'el-popover-stub'),
|
|
ElImage: {template: '<img class="el-image-stub" />'},
|
|
ElProgress: passthrough('div', 'el-progress-stub'),
|
|
ElSlider: {template: '<input type="range" class="el-slider-stub" />'},
|
|
ElTimeline: passthrough('ul', 'el-timeline-stub'),
|
|
ElTimelineItem: {template: '<li class="el-timeline-item-stub"><slot /></li>'},
|
|
};
|