vue 应用测试(一) --- 介绍
vue 应用测试(一) ---介绍
- 前端测试简介
- 组件测试
- Jest 测试框架简介
- 其他测试框架
- 第一个测试
- 避免误报
- 如何组织测试代码
- 组件挂载
- Vue2 组件挂载的方式
- Vue3 的挂载方式
- vue-test-utils
- 挂载选项
- 如何调试测试用例
- 参考
- 小结
前端测试简介
软件测试:检查软件是否按照预期工作的过程。
测试分类:
从是否需要手动测试来分:
- 手动测试:需要人工操作,比如点击按钮,输入文字等。
- 自动测试:写代码测试其他代码,不需要人亲自手动测试每一个功能。
前端测试,从测试的范围来分:
- 端到端测试
测试整个应用,从用户角度出发,浏览器自动测试整个应用是否按照预期工作。是自动执行的手动测试,加快手动测试的速度。
- 优点:测试全面,测试结果可靠。
- 缺点:① 测试速度慢 ② 调试困难 ③ 可能成为 flakey 测试 ④ 编写测试代码的成本高。
flakey 测试:即使程序没有问题,测试也会失败。
- 单元测试
对应用的小部分进行的测试。比如测试一个函数,一个组件等。
- 优点:① 测试速度快 ② 调试方便 ③ 编写测试代码的成本低 ④ 提供文档功能,可通过测试用例了解代码的行为 ⑤ 稳定,有助于重构。
- 缺点:测试范围小,测试结果不可靠。
重构:不改变代码的功能,但是改变代码的结构,目的是为了提高代码质量。
- 快照测试
快照测试会给运行中的应用程序拍一张图片,并将其与以前保存的图片进行比较。如果图像不同,则测试失败。这种测试方法对确保应用程序代码变更后是否仍然可以正确渲染很有帮助。
按照是否测试实现来分类:
- 白盒测试
测试代码的实现,比如测试一个函数,需要知道函数的实现细节和依赖关系,然后编写测试代码。往往是开发人员编写的测试代码。
编写白盒测试,为了隔离复杂的依赖,通常会使用各种模拟手段,来模拟依赖的行为,比如模拟网络请求、用户操作和 pinia 等。
不仅测试做了什么,还要测试怎么做到的。
白盒测试用例往往比较脆弱,一旦代码实现发生变化,测试用例很可能就会失败。
- 黑盒测试
不关心代码的实现,只关心代码的输入和输出,比如测试一个函数,只需要知道函数的输入和输出,然后编写测试代码。往往是测试人员编写的测试代码。
要少用模拟或者恰当的模拟,因为你不知道内部的依赖关系,也难以模拟,二来越多的模拟测试越不可靠。
测试做了什么,而不是测试怎么做的。
黑盒测试用例往往比较稳定,一旦代码实现发生变化,测试用例不会失败。
各种测试在前端测试中的占比

为何没有集成测试?
前端的集成测试,难以定义、编写和调试,通常认为端到端测试就是集成测试。
什么时候不需要自动化测试?
自动化测试的目的是为了节省时间和精力,长期开发的规模较大的项目自动化测试才会带来巨大的收益。如果项目只是一个小项目,或者是一个短期项目,那么自动化测试可能会带来负担,即编写测试代码会比直接编写应用代码更花时间。
实际上,在我的工作中,前端进行自动化测试的团队都很少,大部分都是手动测试。
不必追求 100% 的测试覆盖率
除非一个 bug 导致了严重的后果,比如损失几百万元,否则不必追求 100% 的测试覆盖率。因为测试代码也是需要维护的,测试代码的维护成本也是需要考虑的。
组件测试
组件有很多属性,决定测试哪些属性很重要,能帮助编写高效的测试代码。
如何决定测试哪些属性?
组件的输入和输出(有人叫组件契约或者组件接口)可帮助决定测试哪些属性。
从开发人员使用组件但又不了解组件具体实现的角度来编写测试,好的组件单元测试应该始终可触发一个输入,并断言一个输出。
常见的组件输入:
- 用户操作,比如点击按钮,输入文字等;
- props;
- 组件事件;
- vuex store 中的数据;
- inject 注入的数据。
常见的输出:
- 触发的事件;
- 外部调用的方法,即公有方法;
- 渲染结果。
Jest 测试框架简介
测试文件:以 .spec.js 或者 .test.js 结尾。
Jest 在查找项目中测试文件时使用默认的 glob 匹配模式。对于 non-glob 模式而言,这意味着 Jest 匹配tests目录中的.js 和.jsx 文件,以及扩展名为 .spec.js 或 .test.js 的所有文件。
globs 是文件匹配模式。Jest 使用 Node glob 模块匹配文件。你可以在如下链接页面的 glob primer 部分中阅读到更多关于 globs 的内容,glob-primer。
其他测试框架
vitest
peeky
第一个测试
已经存在一个使用 vue-cli 创建的项目,希望添加测试。
- 安装 vue 测试插件:
vue add @vue/cli-plugin-unit-jest
使用 vue-cli 创建的项目,可以使用
vue add命令安装插件,会自动配置测试环境。
- 编写 HelloWorld.vue 组件:
安装完毕会自动配置测试环境,并创建了一个测试 HelloWorld.vue 的用例,但是项目里没有 HelloWorld.vue,在 tests/unit 就近新建一个。
<script>export default {name: 'HelloWorld',props: {msg: {type: String,default: '',},},data() {return {}},}
</script><template><div>{{ msg }}</div>
</template>
然后引入组件:
import HelloWorld from './HelloWorld.vue'
- 运行测试
执行 npm run test:unit ,测试环境是否配置成功。
可用性(sanity)测试
搭建测试系统的第一步是编写一个简单的测试来检查系统是否配置正确。这被称为可用性(sanity)测试。
在排查复杂问题或者配置环境时,可用性测试应该成为第一个测试用例,因为它能检查环境是否配置正确。
就近放置测试文件
将单元测试放置在尽可能接近被测代码的位置,会更容易被其他开发人员找到。
避免误报
测试中,需要避免误报。测试之所以通过,是因为源代码正常工作,而不是因为编写始终能通过的测试。
常见的误报测试是使用异步代码。
test('sets finished to true after 100ms', () => {runner.start()setTimeout(() => {expect(runner.finished).toBe(true) // 100ms 后 finished 为 true}, 100)
})
避免误报的最好方法是使用 TDD。
红色阶段是编写一个因正确原因而失败的测试。这里的关键词是“因正确原因”,即确定程序失败的边界条件。
测试驱动开发(TDD)是一种在编写源代码之前先编写测试代码的工作流程,即在编写组件代码之前,需要先编写能够确保组件正常运行的测试代码。
“红、绿、重构” 是一种很流行的 TDD 方法。红代表编写一个不能通过的测试,绿代表让测试通过,在测试通过后,通过重构增强代码可读性。
以这样的方式开发应用程序会有如下好处。首先,你只编写测试功能的源代码,从而保持较少的源代码量;其次,它可以使你在编写代码之前先考虑组件设计。
如何组织测试代码
describe 函数用于组织测试代码,describe 用于定义一组测试用例,每个测试用例都是一个 test 函数。
describe 函数将多个单元测试用例定义为一个测试套件。当你在命令行运行测试时,Jest 会格式化输出,以便你了解哪些测试套件通过,哪些测试套件失败。
describe('HelloWorld.vue', () => {it('renders props.msg when passed', () => {const msg = 'new message'const wrapper = shallowMount(HelloWorld, {propsData: {msg},})expect(wrapper.text()).toMatch(msg)})
})
当运行测试时,会在控制台格式化输出 describe 和 test 的一个参数,方便查看测试结果。
一个文件可写多个 describe,describe 可嵌套,
推荐的做法是一个文件只写一个 describe。
否则会降低测试代码的可读性和新加的测试用例的不知道放在哪个 describe 里面。
测试代码和源代码挨近,方便他人查看。
不要嵌套使用 describe,会让测试代码难以理解。
test 表示一个测试用例。
两个参数:
第一个参数是一个 字符串 ,在同一个测试套件中,需要唯一,用于标识测试报告中的测试,用来对你的测试做讲要的说明,方便你的阅读测试报告。
第二个参数是包含测试代码的函数。
it是它的别名xit表示跳过这个测试用例,在跳过某些正在或者不想要测试的用例时特别有用。
test.only表示只运行这个测试用例,其他测试用例都会被跳过。
三步法(3A法则)写测试用例
- 准备测试环境(数据、模拟的函数、模拟模块、挂载组件等,是测试的必要条件)(Arrange),让测试就绪,这里是渲染组件。
- 采取行动(Action),执行某些操作,比如用户输入、查找渲染结果等,这里是获取组件的文本内容。
- 断言(Assert),判断上述行动是否符合预期,这里是断言组件的文本内容和测试数据是否一致,。
以上三步的代码使用空行分隔,这样可以让测试代码更加清晰可读。
编程的经验法则之一:代码排版反映思路。
代码越是美观合理,说明写下这段代码的时候,思路越是清晰,这样的代码也更容易被其他人理解,反之亦然。
组件挂载
Vue 组件想要渲染到页面上,需要一个挂载的动作,或者说触发组件渲染到页面上的动作叫挂载。
Vue2 组件挂载的方式
-
在组件选项中指定 el。
-
使用 Vue 构造器动态挂载。
new Vue(componentOptions).$mount(el)
new Vue.extend(componentOptions).$mount(el)
Vue.extend 接收一个组件选项,然后返回一个构造器。
使用 Vue.extend 手动挂载组件,也是 vue2 中实现弹窗的方式,即新建一个和 body 同级的 div,然后把组件挂载到这个 div,从而让组件的渲染结果脱离组件的嵌套关系。
import Vue from 'vue'
const App = {props: {count: Number,},data() {return {msg: 'hello',innerCount: 0}},methods: {add() {this.innerCount += 1},},template: /*html*/ `<div v-if="count % 2 === 0">count:{{count}}. count is even.</div><div v-else>count:{{count}}. count is odd.</div><button @click="add">{{innerCount}}</button>`,
}describe('App', () => {it('挂载App', () => {const Ctor = new Vue.extend(App)const app = new Ctor()app.$mount()})
})
Vue3 的挂载方式
- createApp
const app = createApp(App)
app.mount('#app')
使用 createApp 挂载一个弹窗。
<template><div class="modal-container">我是弹窗组件<button type="button" @click="close">点击我关闭</button></div>
</template><script>import {ref,onMounted,reactive,watch,computed,defineComponent} from 'vue'export default defineComponent({name: 'Modal',components: {},setup(props, {emit,attrs,slots}) {onMounted(() => {console.log('onMounted')})function close() {const modal = document.querySelector('.modal')document.body.removeChild(modal)}return {close}},})
</script><style>.modal {position: fixed;left: 50%;top: 50%;transform: translate(-50%, -50%);width: 400px;height: 200px;z-index: 1000;background-color: #ccc;}.modal-container {position: relative;left: 50%;top: 50%;transform: translate(-50%, -50%);width: 100px;height: 100px;background-color: red;}
</style>
在某个组件内执行 mountModal 挂载显示弹窗。
function mountModal() {const div = document.createElement('div')div.className = 'modal'document.body.appendChild(div)const modal = createApp(Modal)modal.mount(div)
}
参考问题
- 瞬移组件 Teleport
<template><Teleport to="modal-container"><p>通过to属性指定挂载的DOM</p></Teleport>
</template>
jest 在 jsdom 环境中运行,jsdom 是一个模拟浏览器环境的库,它提供了一些浏览器环境的全局变量,比如 window、document 等。
所以能直接挂载组件。
vue-test-utils
手动挂载组件,代码量较多,vue-test-utils 提供了一些方便的 API 帮我们做这些事情。
mount 方法,该方法在接收一个组件后,会将其挂载并返回一个包含被挂载组件实例(vm)的包装器对象。
知道为什么 mount 不直返回 Vue 实例(vm)而是返回包装器?
mount 返回的包装器不仅包含 Vue 实例,还包括一些辅助方法,你可以使用它们来设置 props,检查实例属性以及操作实例。
常用的包装器方法:
- text 方法:返回包装器的文本内容。
- html 方法:返回包装器的 HTML 内容。
- find 方法:返回包含指定选择器的第一个 DOM 元素的包装器。
- exists 方法:返回一个布尔值,指示包装器是否包含一个或多个匹配的元素。
- findAll 方法:返回包含指定选择器的所有 DOM 元素的包装器。
- setData 方法:设置组件的 data 属性。
- setProps 方法:设置组件的 props 属性。
- trigger 方法:触发指定的事件。
- vm 属性:返回包装器的 Vue 实例。
mount vs shallowMount
另一个挂载方法, shallowMount ,该方法与 mount 方法类似,但是它不会渲染组件的子组件,而是使用 vuecomponent-stub 代替。
它隔离了组件与其子组件的关系,排除了复杂的依赖关系,使得测试更加专注于当前组件。
mount 会渲染子组件,更加贴近真实环境,但是会增加测试的复杂度。
一种不渲染子组件的方 — stubs 模拟子组件。
mount(ParentCom, {stubs: {Child: trueChild2: `<span>我是子组件</span>`}
})
使用 stubs 对象模拟子组件,可以使用布尔值或者字符串模拟子组件。
这些子组件不会真的渲染,但是它们会存在你的包装器中,你可以使用 find 方法找到它们。
挂载选项
挂载时可以传递一些组件选项,比如 propsData、data、mocks 等,这些选项会覆盖组件的默认选项。
Mounting Options
如何调试测试用例
- 使用 vscode 扩展
Jest Runner 可以在 vscode 中运行测试用例,方便调试。
推荐使用
- 在 chrome 浏览器调试
开启 jest 调试模式,新加一个脚本:
"test:debug": "node --inspect-brk node_modules/.bin/vue-cli-service test:unit",
没成功,可能哪儿没弄对。你可以试试。
- 在 vscode 中调试
参考
vue 官方对测试的建议
Jest 单元测试环境搭建
Vue.js unit test cases with vue-test-utils and Jest
Guide to Unit Testing Vue Components
All Vue Content
Vue NYC - Component Tests with Vue.js - Matt O’Connell—技术演讲
An Introduction to testing in Javascript
小结
- 介绍了前端测试的分类,单元测试能帮助我们编写高质量的代码。
- 组件测试的要点:给组件输入,测试输出。
- Jest 测试框架简介。
- 3A法则写测试用例。
- vue 组件渲染的三种方式:shallowMount、mount、Teleport.to 和手动挂载。
- 不渲染子组件的几种方式:stubs 配置、shallowMount。
- 调试测试用例的三种方式:vscode 插件、chrome、vscode。
相关文章:
vue 应用测试(一) --- 介绍
vue 应用测试(一) ---介绍 前端测试简介组件测试Jest 测试框架简介其他测试框架 第一个测试避免误报如何组织测试代码 组件挂载Vue2 组件挂载的方式Vue3 的挂载方式vue-test-utils挂载选项 如何调试测试用例参考小结 前端测试简介 软件测试:…...
Perl 语言入门学习
一、介绍 Perl 是一种高级的、动态的、解释型的通用编程语言,由Larry Wall于1987年开发。它是一种非常灵活和强大的语言,广泛用于文本处理、系统管理、网络编程、图形编程等领域。 Perl 语言的设计理念是“用一种简单的语法,去解决复杂的编…...
HarmongOS打包[保姆级]
创建应用 首先进入 华为开发者联盟-HarmonyOS开发者官网 然后进行登录。 登录成功后,鼠标悬停在在登录右上角那个位置后再点击管理中心,进入下面这个界面。 再点击:应用服务–>应用发布–>新建–>完善信息 构建和生成私钥和证书请求…...
SpringBoot怎么实现自定义接口全局异常捕获?详细教程
自定义异常 package com.single.bean;import org.springframework.core.NestedRuntimeException;public class FDWException extends NestedRuntimeException {private static final long serialVersionUID = 6046035491210083235L;public FDWException(String msg) {super(msg…...
Ms08067安全实验室成功实施多家业务系统渗透测试项目
点击星标,即时接收最新推文 近日,Ms08067安全实验室针对多家公司重要系统实施渗透测试项目。公司网络信息系统的业务应用和存储的重要信息资产均较多,存在网络系统结构的复杂性和庞杂等特点,使得公司网络信息系统面临一定风险。项…...
小熊家政帮day22-day23 订单系统优化(订单状态机、练习分库分表、索引、订单缓存)
目录 1 状态机1.1 状态机介绍1.1.1 当前存在的问题1.1.2 使用状态机解决问题 1.2 实现订单状态机1.2.1 编写订单状态机1.2.1.1 依赖引入1.2.1.2 订单状态枚举类1.2.1.3 状态变更事件枚举类1.2.1.4 定义订单快照类1.2.1.5 定义事件变更动作类1.2.1.5 定义订单状态机类1.2.1.6 状…...
LeetCode 1731, 151, 148
目录 1731. 每位经理的下属员工数量题目链接表要求知识点思路代码 151. 反转字符串中的单词题目链接标签思路代码 148. 排序链表题目链接标签Collections.sort()思路代码 归并排序思路代码 1731. 每位经理的下属员工数量 题目链接 1731. 每位经理的下属员工数量 表 表Emplo…...
Codeforces Round 953 (Div. 2)(A~D题解)
这次比赛是我最顺利的一次比赛,也是成功在中途打进前1500,写完第三道题的时候也是保持在1600左右,但是后面就啥都不会了,还吃了点罚时,虽说如此也算是看到进步了,D题学长说很简单,但是我当时分析…...
晶圆切割机(晶圆划片机)为晶圆加工重要设备 我国市场国产化进程不断加快
晶圆切割机(晶圆划片机)为晶圆加工重要设备 我国市场国产化进程不断加快 晶圆切割机又称晶圆划片机,指能将晶圆切割成芯片的机器设备。晶圆切割机需具备切割精度高、切割速度快、操作便捷、稳定性好等特点,在半导体制造领域应用广…...
39、基于深度学习的(拼音)字符识别(matlab)
1、原理及流程 深度学习中常用的字符识别方法包括卷积神经网络(CNN)和循环神经网络(RNN)。 数据准备:首先需要准备包含字符的数据集,通常是手写字符、印刷字符或者印刷字体数据集。 数据预处理࿱…...
CCF 矩阵重塑
第一题:矩阵重塑(一) 本题有两种思路 第一种 (不确定是否正确 但是100分) #include<iostream> using namespace std; int main(){int n,m,p,q,i,j;cin>>n>>m>>p>>q;int a[n][m];for(i…...
Aigtek高压放大器在柔性爬行机器人驱动性能研究中的应用
实验名称:柔性爬行机器人的材料测试 研究方向:介电弹性体的最小能量结构是一种利用DE材料的电致变形与柔性框架形变相结合设计的新型柔性驱动器,所谓最小能量是指驱动器在平衡状态时整个系统的能量最小,当系统在外界的电压刺激下就…...
Postman下发流表至Opendaylight
目录 任务目的 任务内容 实验原理 实验环境 实验过程 1、打开ODL控制器 2、网页端打开ODL控制页面 3、创建拓扑 4、Postman中查看交换机的信息 5、L2层流表下发 6、L3层流表下发 7、L4层流表下发 任务目的 1、掌握OpenFlow流表相关知识,理解SDN网络中L…...
C语言王国——数组的旋转(轮转数组)三种解法
目录 一、题目 二、分析 2.1 暴力求解法 2.2 找规律 2.3 追求时间效率,以空间换时间 三、结论 一、题目 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出…...
MySQL中CAST和CONVERT函数都用于数据类型转换
在 MySQL 中,CAST() 和 CONVERT() 函数都用于数据类型转换。虽然这两个函数在大多数情况下可以互换使用,但它们之间还是有一些细微的差别。 官方文档地址 https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html#function_cast CAST() 函数 C…...
速盾:cdn影响seo吗?
CDN (Content Delivery Network) 是一个分布式网络架构,用于在全球范围内加速网站内容的传输和分发。它通过将网站的静态资源(例如图片、CSS、JavaScript 文件等)存储在多个服务器上,使用户可以从最接近他们位置的服务器上获取这些…...
期末算法复习
0-1背包问题(动态规划) 例题 算法思想: 动态规划的核心思想是将原问题拆分成若干个子问题,并利用已解决的子问题的解来求解更大规模的问题。 主要是状态转移方程和状态 算法描述: 初始化一个二维数组dp࿰…...
可穿戴设备:苹果“吃老底”、华为“忙复苏”、小米“再扩容”
配图来自Canva可画 随着产品功能的创新,可穿戴设备不再被简单地视为手机的延伸,而是被当成一种独立的、具有独特功能和优势的产品,受到了越来越多人的青睐。 一方面,技术的进步使得可穿戴设备在功能、性能和使用体验上得到显著提…...
AI数据分析:集中度分析和离散度分析
在deepseek中输入提示词: 你是一个Python编程专家,要完成一个Python脚本编写的任务,具体步骤如下: 读取Excel表格:"F:\AI自媒体内容\AI行业数据分析\toolify月榜\toolify2023年-2024年月排行榜汇总数据.xlsx&qu…...
redis的分布式session和本地的session有啥区别
在web应用开发中,Session用于在多个请求之间存储用户数据。传统上,Session存储在服务器的内存中,即本地Session。然而,随着应用规模和复杂度的增加,特别是在分布式环境中,本地Session会遇到一些问题。这时&…...
CxFlatUI——一款开源免费、现代化的 WinForm UI 控件库
文章目录一、前言二、项目概述三、应用场景四、功能模块五、功能特点六、功能演示七、源码地址一、前言 对于仍在使用 WinForms 技术栈构建企业内部系统、工具软件、桌面管理端、工业控制端或数据录入客户端的团队而言,传统 WinForms 默认控件在视觉表现、交互质感…...
英雄联盟R3nzSkin换肤工具:5分钟快速上手免费皮肤解锁指南
英雄联盟R3nzSkin换肤工具:5分钟快速上手免费皮肤解锁指南 【免费下载链接】R3nzSkin-For-China-Server Skin changer for League of Legends (LOL) 项目地址: https://gitcode.com/gh_mirrors/r3/R3nzSkin-For-China-Server 还在为英雄联盟国服昂贵的皮肤价…...
Arm DDT:高性能计算并行程序调试利器
1. Arm DDT调试工具概述Arm DDT(Distributed Debugging Tool)是Arm公司开发的一款专业级并行程序调试工具,专为高性能计算(HPC)领域设计。作为Arm Forge工具套件的重要组成部分,DDT提供了强大的MPI程序调试…...
Photoshop AVIF插件实战:解锁下一代图像格式的完整解决方案
Photoshop AVIF插件实战:解锁下一代图像格式的完整解决方案 【免费下载链接】avif-format An AV1 Image (AVIF) file format plug-in for Adobe Photoshop 项目地址: https://gitcode.com/gh_mirrors/avi/avif-format 为Adobe Photoshop添加AVIF格式支持不再…...
Clawforce:开源AI智能体团队基础设施,实现持久化与安全协作
1. 项目概述:Clawforce,一个为持久化AI智能体团队构建的基础设施最近在AI智能体领域,一个词被反复提及:“Agentic AI”,即智能体驱动的AI。这不再是让单个AI模型回答一个问题那么简单,而是构建一个能够自主…...
在Node.js后端服务中集成Taotoken调用多模型API实战
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 在Node.js后端服务中集成Taotoken调用多模型API实战 构建需要AI能力的Web服务时,后端开发者常面临模型选型、API接入复…...
RDMA之从userspace verbs 到kernel verbs
用户态RDMA(userspace verbs)RDMA是一种高性能网络协议,一般用在GPU集群的高速通信库,如NCCL、NVSHMEM等,这些都是用户态通信库,我们熟知的RDMA大部分都是用户态RDMA。比如,如下一个简单的RDMA程序int main() { // 1…...
外科医生AI认知变迁:从技术好奇到价值驱动的全球调查
1. 项目概述:一场关于外科医生与AI认知变迁的全球对话作为一名长期关注技术与医疗交叉领域的从业者,我始终对一个问题抱有浓厚兴趣:当一项颠覆性技术从实验室走向临床,真正使用它的医生们究竟在想什么?他们的期待、困惑…...
NomNom终极指南:3个技巧让你轻松掌控《无人深空》存档
NomNom终极指南:3个技巧让你轻松掌控《无人深空》存档 【免费下载链接】NomNom NomNom is the most complete savegame editor for NMS but also shows additional information around the data youre about to change. You can also easily look up each item indi…...
终极Moonlight TV游戏串流指南:3分钟实现电视大屏游戏体验
终极Moonlight TV游戏串流指南:3分钟实现电视大屏游戏体验 【免费下载链接】moonlight-tv Lightweight NVIDIA GameStream Client, for LG webOS TV and embedded devices like Raspberry Pi 项目地址: https://gitcode.com/gh_mirrors/mo/moonlight-tv 你是…...
