本文为来自飞书 aPaaS Growth 研发团队成员的文章。
aPaaS Growth 团队专注在用户可感知的、宏观的 aPaaS 应用的搭建流程,及租户、应用治理等产品路径,致力于打造 aPaaS 平台流畅的 “应用交付” 流程和体验,完善应用构建相关的生态,加强应用搭建的便捷性和可靠性,提升应用的整体性能,从而助力 aPaaS 的用户增长,与基础团队一起推进 aPaaS 在企业内外部的落地与提效。
// mocha-demo/index.js
const toString = Object.prototype.toString;
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
module.exports = {
getTag,
};
上述代码使用了Object.prototype.toString来判断了数据类型,我们针对上述代码的测试用例(此处断言使用node原生的assert方法,采用BDD的测试风格):
// test/getTag.spec.js
const assert = require('assert');
const { getTag } = require('../index');
describe('检查:getTag函数执行', function () {
before(function() {
console.log('before钩子触发');
});
describe('测试:正常流', function() {
it('类型返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('类型返回: [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('测试:异常流', function() {
it('类型返回: [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
});
after(function() {
console.log('after钩子触发');
});
});
mocha提供的api语义还是比较强的,即使没写过单元测试代码,单看这段代码也不难理解这段代码干了啥,而这段测试代码页会作为我们最后验证简易Mocha的样例,我们先来看下使用mocha运行该测试用例的执行结果:
如上图所示,即我们前面测试代码的执行结果,我们来拆分下当前mocha实现的一些功能点。
注:mocha更多使用方法可参考Mocha - the fun, simple, flexible JavaScript test framework[1]
describe
和 it
来进行测试用例的编写。describe
函数我们称之为测试套件,它的核心功能是来描述测试的流程,it
函数我们称之为一个测试单元,它的功能是来执行具体的测试用例。it('类型返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
这种异步代码在我们实际业务中也是十分常见的,比如某一部分代码依赖接口数据的返回,或是对某些定时器进行单测用例的编写。mocha支持两种方式的异步代码,一种是回调函数直接返回一个Promise,一种是支持在回调函数中传参数done,手动调用done函数来结束用例。
describe
、it
和before/after
都无需引用依赖,直接调用即可,因此我们还要实现下相关 api 的全局挂载。├── index.js #待测试代码(业务代码)
├── mocha #简易mocha所在目录
│ ├── index.js #简易mocha入口文件
│ ├── interfaces #存放不同的测试风格
│ │ ├── bdd.js #BDD 测试风格的实现
│ │ └── index.js #方便不同测试风格的导出
│ ├── reporters #生成测试报告
│ │ ├── index.js
│ │ └── spec.js
│ └── src #简易mocha核心目录
│ ├── mocha.js #存放Mocha类控制整个流程
│ ├── runner.js #Runner类,辅助Mocha类执行测试用例
│ ├── suite.js #Suite类,处理describe函数
│ ├── test.js #Test类,处理it函数
│ └── utils.js #存放一些工具函数
├── package.json
└── test #测试用例编写
└── getTag.spec.js
上面的mocha文件夹就是我们将要实现的简易版mocha目录,目录结构参考的mocha源码,但只采取了核心部分目录结构。
class Mocha {
constructor() {}
run() {}
}
module.exports = Mocha;
入口文件更新为:
// mocha-demo/mocha/index.js
const Mocha = require('./src/mocha');
const mocha = new Mocha();
mocha.run();
测试用例的执行过程顺序尤其重要,前面说过用例的执行遵循从外到里,从上到下的顺序,对于describe
和it
的回调函数处理很容易让我们想到这是一个树形结构,而且是深度优先的遍历顺序。简化下上面的用例代码:
describe('检查:getTag函数执行', function () {
describe('测试:正常流', function() {
it('类型返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('类型返回: [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('测试:异常流', function() {
it('类型返回: [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
});
});
针对这段代码结构如下:
image.png
整个树的结构如上,而我们在处理具体的函数的时候则可以定义Suite/Test两个类来分别描述describe/it
两个函数。可以看到describe函数是存在父子关系的,关于Suite类的属性我们定义如下:
// mocha/src/suite.js
class Suite {
/**
*
* @param { * } parent 父节点
* @param { * } title Suite名称,即describe传入的第一个参数
*/
constructor ( parent, title ) {
this . title = title; // Suite名称,即describe传入的第一个参数
this . parent = parent // 父suite
this . suites = []; // 子级suite
this . tests = []; // 包含的it 测试用例方法
this . _beforeAll = []; // before 钩子
this . _afterAll = []; // after 钩子
this . _beforeEach = []; // beforeEach钩子
this . _afterEach = []; // afterEach 钩子
// 将当前Suite实例push到父级的suties数组中
if (parent instanceof Suite ) {
parent. suites . push ( this );
}
}
}
module . exports = Suite ;
而Test类代表it就可以定义的较为简单:
// mocha/src/test.js
class Test {
constructor(props) {
this.title = props.title; // Test名称,it传入的第一个参数
this.fn = props.fn; // Test的执行函数,it传入的第二个参数
}
}
module.exports = Test;
此时我们整个流程就出来了:
1 . 收集用例(通过Suite和Test类来构造整棵树);
2 . 执行用例(遍历这棵树,执行所有的用例函数);
3 . 收集测试用例的执行结果。
a . 此时我们整个的流程如下(其中执行测试用例和收集执行结果已简化):
image.png
OK,思路已经非常清晰,实现一下具体的代码吧
// mocha/src/mocha.js
const Suite = require('./suite');
class Mocha {
constructor() {
// 创建根节点
this.rootSuite = new Suite(null, '');
}
run() { }
}
module.exports = Mocha;
// mocha/interfaces/bdd.js
// context是我们的上下文环境,root是我们的树的根节点
module.exports = function (context, root) {
// context是describe的别名,主要目的是处于测试用例代码的组织和可读性的考虑
context.describe = context.context = function(title, fn) {}
// specify是it的别名
context.it = context.specify = function(title, fn) {}
context.before = function(fn) {}
context.after = function(fn) {}
context.beforeEach = function(fn) {}
context.afterEach = function(fn) {}
}
为方便支持各种测试风格接口我们进行统一的导出:
// mocha/interfaces/index.js
'use strict';
exports.bdd = require('./bdd');
然后在Mocha类中进行bdd接口的全局挂载:
// mocha/src/mocha.js
const interfaces = require('../interfaces');
class Mocha {
constructor() {
// this.rootSuite = ...
// 注意第二个参数是我们的前面创建的根节点,此时
interfaces['bdd'](global, this.rootSuite "'bdd'");
}
run() {}
}
module.exports = Mocha;
此时我们已经完成了api的全局挂载,可以放心导入测试用例文件让函数执行了。
// mocha/src/utils.js
const path = require('path');
const fs = require('fs');
/**
*
* @param { * } filepath 文件或是文件夹路径
* @returns 所有测试文件路径数组
*/
module.exports.findCaseFile = function (filepath) {
function readFileList(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach((item, _ ) => {
var fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
readFileList(path.join(dir, item), fileList); // 递归读取文件
} else {
fileList.push(fullPath);
}
});
return fileList;
}
let fileList = [];
// 路径如果是文件则直接返回
try {
const stat = fs.statSync(filepath);
if (stat.isFile()) {
fileList = [filepath];
return fileList;
}
readFileList(filepath, fileList);
} catch(e) {console.log(e)}
return fileList;
}
上面函数简单的实现了一个方法,用来递归的读取本地所有的测试用例文件,然后在Mocha类中使用该方法加载我们当前的测试用例文件:
// mocha/src/mocha.js
const path = require('path');
const interfaces = require('../interfaces');
const utils = require('./utils');
class Mocha {
constructor() {
// this.rootSuite = ...
// interfaces['bdd'](global, this.rootSuite "'bdd'");
// 写死我们本地测试用例所在文件夹地址
const spec = path.resolve(__dirname, '../../test');
const files = utils.findCaseFile(spec);
// 加载测试用例文件
files.forEach(file => require(file));
}
run() {}
}
module.exports = Mocha;
describe
和it
还都是个空函数,我们接下来修改下我们提供的describe和it函数,来创建我们需要的树形结构,在前面我们已经在bdd.js文件中对describe和it进行了初始化,此时补充上我们借用栈创建Suite-Test树的逻辑:// mocha/interfaces/bdd.js
const Suite = require('../src/suite');
const Test = require('../src/test');
module.exports = function (context, root) {
// 树的根节点进栈
const suites = [root];
// context是describe的别名,主要目的是处于测试用例代码的组织和可读性的考虑
context.describe = context.context = function (title, callback) {
// 获取当前栈中的当前节点
const cur = suites[0];
// 实例化一个Suite对象,存储当前的describe函数信息
const suite = new Suite(cur, title);
// 入栈
suites.unshift(suite);
// 执行describe回调函数
callback.call(suite);
// Suite出栈
suites.shift();
}
context.it = context.specify = function (title, fn) {
// 获取当前Suite节点
const cur = suites[0];
const test = new Test(title, fn);
// 将Test实例对象存储在tests数组中
cur.tests.push(test);
}
// ...
}
注意,上面的代码我们仅仅是通过执行describe的回调函数将树的结构创建了出来,里面具体的测试用例代码(it的回调函数)还未开始执行。基于以上代码,我们整个Suite-Test树就已经创建出来了,截止到目前的代码我们收集用例的过程已经实现完成。此时我们的Sute-Test树创建出来是这样的结构:
image.png
// mocha/src/utils.js
const path = require('path');
const fs = require('fs');
// module.exports.findCaseFile = ...
module.exports.adaptPromise = function(fn) {
return () => new Promise(resolve => {
if (fn.length === 0) {
// 不使用参数 done
try {
const ret = fn();
// 判断是否返回promise
if (ret instanceof Promise) {
return ret.then(resolve, resolve);
} else {
resolve();
}
} catch (error) {
resolve(error);
}
} else {
// 使用参数 done
function done(error) {
resolve(error);
}
fn(done);
}
})
}
我们改造下之前创建的Suite-Test树,将it、before、after、beforeEach和afterEach的回调函数进行适配:
// mocha/interfaces/bdd.js
const Suite = require('../src/suite');
const Test = require('../src/test');
const { adaptPromise } = require('../src/utils');
module.exports = function (context, root) {
const suites = [root];
// context是describe的别名,主要目的是处于测试用例代码的组织和可读性的考虑
// context.describe = context.context = ...
context.it = context.specify = function (title, fn) {
const cur = suites[0];
const test = new Test(title, adaptPromise(fn));
cur.tests.push(test);
}
context.before = function (fn) {
const cur = suites[0];
cur._beforeAll.push(adaptPromise(fn));
}
context.after = function (fn) {
const cur = suites[0];
cur._afterAll.push(adaptPromise(fn));
}
context.beforeEach = function (fn) {
const cur = suites[0];
cur._beforeEach.push(adaptPromise(fn));
}
context.afterEach = function (fn) {
const cur = suites[0];
cur._afterEach.push(adaptPromise(fn));
}
}
// mocha/src/runner.js
class Runner {}
此时梳理下测试用例的执行逻辑,基于以上创建的Suite-Test树,我们可以对树进行一个遍历从而执行所有的测试用例,而对于异步代码的执行我们可以借用async/await
来实现。此时我们的流程图更新如下:
image.png
整个思路梳理下来就很简单了,针对Suite-Test树,从根节点开始遍历这棵树,将这棵树中所有的Test节点所挂载的回调函数进行执行即可。相关代码实现如下:
// mocha/src/runner.js
class Runner {
constructor() {
super();
// 记录 suite 根节点到当前节点的路径
this.suites = [];
}
/*
* 主入口
*/
async run(root) {
// 开始处理Suite节点
await this.runSuite(root);
}
/*
* 处理suite
*/
async runSuite(suite) {
// 1.执行before钩子函数
if (suite._beforeAll.length) {
for (const fn of suite._beforeAll) {
const result = await fn();
}
}
// 推入当前节点
this.suites.unshift(suite);
// 2. 执行test
if (suite.tests.length) {
for (const test of suite.tests) {
// 执行test回调函数
await this.runTest(test);
}
}
// 3. 执行子级suite
if (suite.suites.length) {
for (const child of suite.suites) {
// 递归处理Suite
await this.runSuite(child);
}
}
// 路径栈推出节点
this.suites.shift();
// 4.执行after钩子函数
if (suite._afterAll.length) {
for (const fn of suite._afterAll) {
// 执行回调
const result = await fn();
}
}
}
/*
* 处理Test
*/
async runTest(test) {
// 1. 由suite根节点向当前suite节点,依次执行beforeEach钩子函数
const _beforeEach = [].concat(this.suites).reverse().reduce((list, suite) => list.concat(suite._beforeEach), []);
if (_beforeEach.length) {
for (const fn of _beforeEach) {
const result = await fn();
}
}
// 2. 执行测试用例
const result = await test.fn();
// 3. 由当前suite节点向suite根节点,依次执行afterEach钩子函数
const _afterEach = [].concat(this.suites).reduce((list, suite) => list.concat(suite._afterEach), []);
if (_afterEach.length) {
for (const fn of _afterEach) {
const result = await fn();
}
}
}
}
module.exports = Runner;
将Runner类注入到Mocha类中:
// mocha/src/mocha.js
const Runner = require('./runner');
class Mocha {
// constructor()..
run() {
const runner = new Runner();
runner.run(this.rootSuite);
}
}
module.exports = Mocha;
简单介绍下上面的代码逻辑,Runner类包括两个方法,一个方法用来处理Suite,一个方法用来处理Test,使用栈的结构遍历Suite-Test树,递归处理所有的Suite节点,从而找到所有的Test节点,将Test中的回调函数进行处理,测试用例执行结束。但到这里我们会发现,只是执行了测试用例而已,测试用例的执行结果还没获取到,测试用例哪个通过了,哪个没通过我们也无法得知。
我们需要一个中间人来记录下执行的结果,输出给我们,此时我们的流程图更新如下:
修改Runner类,让它继承EventEmitter,来实现事件的传递工作:
// mocha/src/runner.js
const EventEmitter = require('events').EventEmitter;
// 监听事件的标识
const constants = {
EVENT_RUN_BEGIN: 'EVENT_RUN_BEGIN', // 执行流程开始
EVENT_RUN_END: 'EVENT_RUN_END', // 执行流程结束
EVENT_SUITE_BEGIN: 'EVENT_SUITE_BEGIN', // 执行suite开始
EVENT_SUITE_END: 'EVENT_SUITE_END', // 执行suite结束
EVENT_FAIL: 'EVENT_FAIL', // 执行用例失败
EVENT_PASS: 'EVENT_PASS' // 执行用例成功
}
class Runner extends EventEmitter {
// ...
/*
* 主入口
*/
async run(root) {
this.emit(constants.EVENT_RUN_BEGIN);
await this.runSuite(root);
this.emit(constants.EVENT_RUN_END);
}
/*
* 执行suite
*/
async runSuite(suite) {
// suite执行开始
this.emit(constants.EVENT_SUITE_BEGIN, suite);
// 1. 执行before钩子函数
if (suite._beforeAll.length) {
for (const fn of suite._beforeAll) {
const result = await fn();
if (result instanceof Error) {
this.emit(constants.EVENT_FAIL, `"before all" hook in ${suite.title}: ${result.message}`);
// suite执行结束
this.emit(constants.EVENT_SUITE_END);
return;
}
}
}
// ...
// 4. 执行after钩子函数
if (suite._afterAll.length) {
for (const fn of suite._afterAll) {
const result = await fn();
if (result instanceof Error) {
this.emit(constants.EVENT_FAIL, `"after all" hook in ${suite.title}: ${result.message}`);
// suite执行结束
this.emit(constants.EVENT_SUITE_END);
return;
}
}
}
// suite结束
this.emit(constants.EVENT_SUITE_END);
}
/*
* 处理Test
*/
async runTest(test) {
// 1. 由suite根节点向当前suite节点,依次执行beforeEach钩子函数
const _beforeEach = [].concat(this.suites).reverse().reduce((list, suite) => list.concat(suite._beforeEach), []);
if (_beforeEach.length) {
for (const fn of _beforeEach) {
const result = await fn();
if (result instanceof Error) {
return this.emit(constants.EVENT_FAIL, `"before each" hook for ${test.title}: ${result.message}`)
}
}
}
// 2. 执行测试用例
const result = await test.fn();
if (result instanceof Error) {
return this.emit(constants.EVENT_FAIL, `${test.title}`);
} else {
this.emit(constants.EVENT_PASS, `${test.title}`);
}
// 3. 由当前suite节点向suite根节点,依次执行afterEach钩子函数
const _afterEach = [].concat(this.suites).reduce((list, suite) => list.concat(suite._afterEach), []);
if (_afterEach.length) {
for (const fn of _afterEach) {
const result = await fn();
if (result instanceof Error) {
return this.emit(constants.EVENT_FAIL, `"after each" hook for ${test.title}: ${result.message}`)
}
}
}
}
}
Runner.constants = constants;
module.exports = Runner
在测试结果的处理函数中监听执行结果的回调进行统一处理:
// mocha/reporter/sped.js
const constants = require('../src/runner').constants;
const colors = {
pass: 90,
fail: 31,
green: 32,
}
function color(type, str) {
return '\u001b[' + colors[type] + 'm' + str + '\u001b[0m';
}
module.exports = function (runner) {
let indents = 0;
let passes = 0;
let failures = 0;
let time = +new Date();
function indent(i = 0) {
return Array(indents + i).join(' ');
}
// 执行开始
runner.on(constants.EVENT_RUN_BEGIN, function() {});
// suite执行开始
runner.on(constants.EVENT_SUITE_BEGIN, function(suite) {
++indents;
console.log(indent(), suite.title);
});
// suite执行结束
runner.on(constants.EVENT_SUITE_END, function() {
--indents;
if (indents == 1) console.log();
});
// 用例通过
runner.on(constants.EVENT_PASS, function(title) {
passes++;
const fmt = indent(1) + color('green', ' ✓') + color('pass', ' %s');
console.log(fmt, title);
});
// 用例失败
runner.on(constants.EVENT_FAIL, function(title) {
failures++;
const fmt = indent(1) + color('fail', ' × %s');
console.log(fmt, title);
});
// 执行结束
runner.once(constants.EVENT_RUN_END, function() {
console.log(color('green', ' %d passing'), passes, color('pass', `(${Date.now() - time}ms)`));
console.log(color('fail', ' %d failing'), failures);
});
}
上面代码的作用对代码进行了收集。
我们再手动构造一个失败用例:
const assert = require('assert');
const { getTag } = require('../index');
describe('检查:getTag函数执行', function () {
before(function() {
console.log('before钩子触发');
});
describe('测试:正常流', function() {
it('类型返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('类型返回: [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('测试:异常流', function() {
it('类型返回: [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
it('类型返回: [object Object]', function() {
assert.equal(getTag([]), '[object Object]');
});
});
after(function() {
console.log('after钩子触发');
});
});
执行下:
一个精简版mocha就此完成!
https://github.com/mochajs/mocha
https://mochajs.org/
[1]Mocha - the fun, simple, flexible JavaScript test framework: https://mochajs.org/
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/tpaCOMlVW8kV--s5SflvwQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。