wenmo8 发布的文章

原始状态的 activemq-client sdk 集成非常方便,也更适合定制。就是有些同学,可能对原始接口会比较陌生,会希望有个具体的示例。

<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-client</artifactId>
    <version>${activemq.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-pool</artifactId>
    <version>${activemq.version}</version>
</dependency>

希望更加简化使用的同学,可以使用:

activemq-solon-cloud-plugin
(使用更简单,定制性弱些)

1、添加集成配置

先使用
Solon 初始器
先生成一个 Solon Web 模板项目,然后添加上面的 activemq-client 依赖。再做个配置约定(也可按需定义):

  • "solon.activemq",作为配置前缀
    • "properties",作为公共配置
    • "producer",作为生态者专属配置(估计用不到)
    • "consumer",作为消费者专属配置(估计用不到)

具体的配置属性,参考自:ActiveMQConnectionFactory

solon.app:
  name: "demo-app"
  group: "demo"

# 配置可以自由定义,与 @Bean 代码对应起来即可(以下为参考)
solon.activemq:
  properties:  #公共配置(配置项,参考:ActiveMQConnectionFactory)
    brokerURL: "failover:tcp://localhost:61616"
    redeliveryPolicy:
      initialRedeliveryDelay: 5000
      backOffMultiplier: 2
      useExponentialBackOff: true
      maximumRedeliveries: -1
      maximumRedeliveryDelay: 3600_000

添加 java 配置器

@Configuration
public class ActivemqConfig {
    @Bean(destroyMethod = "stop")
    public Connection client(@Inject("${solon.activemq.properties}") Props common) throws Exception {
        String brokerURL = (String) common.remove("brokerURL");
        String userName = (String) common.remove("userName");
        String password = (String) common.remove("password");

        ActiveMQConnectionFactory factory;
        if (Utils.isEmpty(userName)) {
            factory = new ActiveMQConnectionFactory(brokerURL);
        } else {
            factory = new ActiveMQConnectionFactory(brokerURL, userName, password);
        }

        //绑定额外的配置并创建连接
        Connection connection = common.bindTo(factory).createConnection();
        connection.start();
        return connection;
    }

    @Bean
    public IProducer producer(Connection connection) throws Exception {
        return new IProducer(connection);
    }

    @Bean
    public void consumer(Connection connection,
                         MessageListener messageListener) throws Exception {
        Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);

        Destination destination = session.createTopic("topic.test");
        MessageConsumer consumer = session.createConsumer(destination);

        consumer.setMessageListener(messageListener);
    }
}

activemq 的消息发送的代码比较复杂,所以我们可以做个包装处理(用于上面的配置构建),临时命名为 IProducer:

public class IProducer {
    private Connection connection;

    public IProducer(Connection connection) {
        this.connection = connection;
    }

    public void send(String topic, MessageBuilder messageBuilder) throws JMSException {
        Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);

        Destination destination = session.createTopic(topic);
        MessageProducer producer = session.createProducer(destination);

        producer.send(destination, messageBuilder.build(session));
    }

    @FunctionalInterface
    public static interface MessageBuilder {
        Message build(Session session) throws JMSException;
    }
}

3、代码应用

发送(或生产),这里代控制器由用户请求再发送消息(仅供参考):

@Controller
public class DemoController {
    @Inject
    private IProducer producer;

    @Mapping("/send")
    public void send(String msg) throws Exception {
        //发送
        producer.send("topic.test", s -> s.createTextMessage("test"));
    }
}

监听(或消费),这里采用订阅回调的方式:(仅供参考)

@Component
public class DemoMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        System.out.println(message);
        RunUtil.runAndTry(message::acknowledge);
    }
}

关注我的朋友应该有印象,这是我不止一次使用这个标题了,之后估计也还会使用这个标题,因为这句话时常出现在我的脑海里,也是我的真实想法。

和之前一样,每隔一段时间,我会给大家分享一下自己的创业过程、所思所想。


上次的分享
中,我提到团队终于走向正轨了,很多事情我都可以脱手交给团队同学去做,不用亲力亲为。那就先分享下最近我们团队和我个人在做的一些事情。

最近一个月,
编程导航
的 APP 已经进行了 4 波内测,我们的开发同学陆陆续续优化了几十个点,这个月应该就可以大范围公测上线了。不过距离上架应用商店应该还有一段时间,审核流程比较严格,而且这个时间不是由我们能够控制的,就比较头疼。

面试鸭刷题神器
这个月增长和月活比较稳定,毕竟秋招刚刚结束,也不是求职高峰期,大家刷面试题的热情有所下降。不过马上寒假就要到了,寒假过后就是春招,还是有不少同学想要弯道超车的。为了激励大家坚持无痛学习,我们也推出了寒假面试通关营,连续 30 天带大家刷题进步,感兴趣的同学
可以了解下

部分学习计划

我自己最近绝大多数时间都放在了
新项目教程
上,白天写教程,晚上直播讲解。才两周的时间,视频教程已经录制将近 30 个小时,这期间也在持续更新文字教程,也已经写了 10 万多字了,妥妥一本书的量!

感受一下新项目的功能

文字教程的分量

当然,代价就是身体了,具体情况不说了,但根据我的经验:足贴、枸杞、胖大海、艾灸贴、红枣、泡脚的确有用,觉得平时没精神的朋友可以试试。

最近公司倒也没出什么大事,但我还是挺愁的,压力山大,现在的压力更多的是源于外部环境的。正所谓不进则退,同行都在卷,你做好了一个东西吧,马上就会被同行抄袭、更有甚者抄袭完后还把你拉黑,也会有很多恶意的竞争和诋毁。这就好比你开一家饭店,老老实实经营,隔壁看你生意好,派个 “卧底” 来学你的配方,完事还顺便丢给你个差评。

所以必须要不断产出新的内容、产品和创意。
如今 AI 是个好东西,的确提高了我们工作和获取信息的效率,但也大大加剧了竞争,增加了创新的难度。

这也是我们团队目前面临的大问题,大家更多地还是利用我自身的资源来盈利,而不是产生新的创意和盈利点。当然这对团队的要求确实是比较高了,所以我还是更多地把这部分压力给到了自己。

但很可怕的事情是,我发现只要长期沉浸在同一个工作中,思维是会固化的,灵感是会被束缚的!就像我这段时间日复一日写教程录讲解,视频也没空做,感觉时间过得飞快,人也有点麻了,创意也比之前少了很多。

好在我意识到了这点,并且想办法调整自己的状态。像我最近只要睡前闭上眼睛,大脑就开始高速运转了,各种杂七杂八的想法和事物如 DDOS 攻击般朝我涌来!如果有好的想法,我立刻再打开手机记录,然后接着闭眼,有时候过一两个小时才能睡着。

记得我在刚创业的时候跟一位融资千万的大佬交流,问他怎么睡个好觉,他笑一笑跟我说:

“换个好枕头”

当时我不理解,现在我已经感同身受了。

现在我已经意识到创业成功必须有的特质:源源不断地创造力、平稳的心态和强大的抗压能力。

像我就经常安慰自己:哥就是来交学费的嘛,哪怕最后公司干黄了,也是一段独特的经历不是么?

不过安慰归安慰,愁还是愁,灵感总不会从天上来,我头顶上悬着的,是每月巨额的支出这把达摩克里斯之剑。

好在我的热情并没有退却,我觉得创业最重要的事情就是你本身要对创业充满热情、并且有信心。如果你自己都觉得,干干试试、随便玩玩,我觉得就不要来趟这趟浑水了。

现在大家越来越浮躁,我也见过太多人被流量和米冲昏了头脑,其实经历过肠胃病和手术之后,这些对我来说都没有那么重要了。接下来我只想在做好我该做的事情的前提下,探索更多的新事物,继续发挥我的热情去做我自己感兴趣的事情,每天保持 Full of Energy(充满能量),我相信问题总会被解决的吧。

感谢你看到了这里,这篇文章可能只是我的一个记录,但如果对你有启发,那我觉得真的是 tai ku la!

最后悄悄透露,我即将实现自己多年来的一个 Big Plan,也算是我人生一个新阶段的尝试。大家可以关注一下,你绝对猜不到我做了什么。。。

更多编程学习资源

用户购买某种产品时习惯一次性付款,但是对开发者而言,单次购买模式或需要用户频繁续订的服务可能会导致收入不稳定,无法获得持续稳定的收入。对于有视频、音乐等会员需求的用户,一旦体验到服务中断或需要频繁操作,可能会转向其他竞争产品,导致用户流失。

HarmonyOS SDK应用内支付服务(IAP Kit)为开发者提供应用内
自动续期订阅商品能力
,用户购买后在一段时间内允许访问增值功能或内容,周期结束后可以选择自动续期购买下一期的服务。此外,IAP Kit提供全面的
订阅服务管理
,涵盖订阅商品、订阅关系、周期、促销、价格和通知等方面,帮助您创造持续稳定的收入。

image

开发步骤

在接入订阅前,开发者需在华为AppGallery Connect网站
配置自动续期订阅商品
,录入商品ID和商品价格等信息。用户在应用购买自动续期订阅商品时,应用需要调用createPurchase接口来拉起IAP Kit订阅收银台,收银台会展示商品名称、商品价格、商品续期计划等信息,用户可在收银台完成商品购买。

1.判断当前登录的华为账号所在的服务地是否支持应用内支付。

在使用应用内支付之前,应用需要向IAP Kit发送queryEnvironmentStatus请求,以此判断用户当前登录的华为账号所在的服务地是否在IAP Kit支持结算的国家/地区中。

import { iap } from '@kit.IAPKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

queryEnvironmentStatus() {
  const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  iap.queryEnvironmentStatus(context).then(() => {
    // 请求成功
    console.info('Succeeded in querying environment status.');
  }).catch((err: BusinessError) => {
    // 请求失败
    console.error(`Failed to query environment status. Code is ${err.code}, message is ${err.message}`);
  });
}

2.查询商品信息

通过queryProducts来获取在AppGallery Connect上配置的商品信息。发起请求时,需在请求参数QueryProductsParameter中携带相关的商品ID,并指定其productType为iap.ProductType.AUTORENEWABLE。

当接口请求成功时,IAP Kit将返回商品信息Product的列表。 应用可以使用Product包含的商品价格、名称和描述等信息,向用户展示可供购买的商品列表。

import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';

queryProducts() {
  const queryProductParam: iap.QueryProductsParameter = {
    productType: iap.ProductType.AUTORENEWABLE,
    productIds: ['product1', 'product2', 'product3']
  };
  const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  iap.queryProducts(context, queryProductParam).then((result) => {
    // 请求成功
    console.info('Succeeded in querying products.');
    // 展示商品信息
    // ...
  }).catch((err: BusinessError) => {
    // 请求失败
    console.error(`Failed to query products. Code is ${err.code}, message is ${err.message}`);  
  });
}

3.发起购买

用户发起购买时,应用可通过向IAP Kit发送createPurchase请求来拉起IAP Kit收银台。发起请求时,应用需在请求参数PurchaseParameter中携带此前已在华为AppGallery Connect网站上配置并生效的自动续期订阅的商品ID,并指定其productType为iap.ProductType.AUTORENEWABLE。

当用户购买成功时,应用将接收到一个CreatePurchaseResult对象,其purchaseData字段包括了此次购买的结果信息。可参见对返回结果验签对PurchaseData.jwsSubscriptionStatus进行解码验签,成功后可得到SubGroupStatusPayload的JSON字符串。

当用户购买失败时,需要针对code为iap.IAPErrorCode.PRODUCT_OWNED和iap.IAPErrorCode.SYSTEM_ERROR的场景,检查是否需要补发货,确保权益发放,具体请参见
权益发放

import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';
// JWTUtil为自定义类,可参见Sample Code工程。
import { JWTUtil } from '../commom/JWTUtil';

subscribe() {
  const createPurchaseParam: iap.PurchaseParameter = {
    // 购买的商品必须是开发者在AppGallery Connect网站配置的商品
    productId: 'test001',
    productType: iap.ProductType.AUTORENEWABLE
  }
  const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  iap.createPurchase(context, createPurchaseParam).then(async (result) => {
    console.info('Succeeded in creating purchase.');
    const jwsSubscriptionStatus: string = JSON.parse(result.purchaseData).jwsSubscriptionStatus;
    if (!jwsSubscriptionStatus) {
      return;
    }
    const subscriptionStatus: string = JWTUtil.decodeJwtObj(jwsSubscriptionStatus);
    if (!subscriptionStatus) {
       return;
    }
    // 需自定义SubGroupStatusPayload类,包含的信息请参见SubGroupStatusPayload
    const subGroupStatusPayload: SubGroupStatusPayload = JSON.parse(subscriptionStatus);
    const lastSubscriptionStatus = subGroupStatusPayload.lastSubscriptionStatus;
    if (!lastSubscriptionStatus || !lastSubscriptionStatus.status) {
      return;
    }
    const purchaseOrderPayload = lastSubscriptionStatus.lastPurchaseOrder;
    if (purchaseOrderPayload === undefined) {
      return;
    }
    // 处理发货
    // ...
    // 发货成功后向IAP Kit发送finishPurchase请求,确认发货,完成购买
    // finishPurchase请求的参数来源于lastSubscriptionStatus.lastPurchaseOrder
    // 发起finishPurchase请求
    // ...
  }).catch((err: BusinessError) => {
    // 请求失败
    console.error(`Failed to create purchase. Code is ${err.code}, message is ${err.message}`);
    if (err.code === iap.IAPErrorCode.PRODUCT_OWNED || err.code === iap.IAPErrorCode.SYSTEM_ERROR) {
      // 参见确保权益发放检查是否需要补发货,确保权益发放
      // ...
    }
  })
}

2.完成购买

对PurchaseData.jwsSubscriptionStatus解码验签成功后,需要进一步判断订阅状态是否是生效中来决定是否发放权益。检查SubGroupStatusPayload.lastSubscriptionStatus.status是否为1(生效中),是则发放相关权益。如果开发者同时接入了
服务端关键事件通知
,为了避免重复发货,建议先检查此笔订单是否已发货,未发货再发放相关权益。发货成功后记录SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder等信息,用于后续检查是否已发货。

发货成功后,应用需发送finishPurchase请求确认发货,以此通知IAP服务器更新商品的发货状态,完成购买流程。发送finishPurchase请求时,需在请求参数FinishPurchaseParameter中携带PurchaseOrderPayload中的productType、purchaseToken、purchaseOrderId,其中PurchaseOrderPayload为SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder。请求成功后,IAP服务器会将相应商品标记为已发货。

import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 确认发货,完成购买
 *
 * @param purchaseOrder 购买数据,来源于购买请求
 */
finishPurchase(purchaseOrder: PurchaseOrderPayload) {
  const finishPurchaseParam: iap.FinishPurchaseParameter = {
    productType: purchaseOrder.productType,
    purchaseToken: purchaseOrder.purchaseToken,
    purchaseOrderId: purchaseOrder.purchaseOrderId
  };
  const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  iap.finishPurchase(context, finishPurchaseParam).then(() => {
    // 请求成功
    console.info('Succeeded in finishing purchase.');
  }).catch((err: BusinessError) => {
    // 请求失败
    console.error(`Failed to finish purchase. Code is ${err.code}, message is ${err.message}`);
  });
}

了解更多详情>>

访问
应用内支付服务联盟官网

获取
接入订阅开发指导文档

1.简介

随着网页的复杂性和动态性的增加,自动化测试变得越来越重要。Playwright作为一款强大的无头浏览器测试库,提供了多种元素定位方式,使得我们能够轻松地对网页进行自动化操作。在基础的定位方式如通过id、class name和tag name等之外,Playwright还提供了更高级的定位技巧,如nth()、first、last和filter()等。本文将对这些高级定位方式进行深入探讨,帮助读者更好地理解和应用这些技术。

2.nth():基于索引的元素定位

在网页中,有时我们会遇到多个具有相同属性或文本的元素,这时我们就需要通过索引来选择特定的元素。Playwright的nth()方法正是为此而生。nth()方法接受一个索引参数,从0开始计数,返回指定索引位置的元素。根据元素索引来选择元素,当符合定位信息的元素有多个时,我们通常要挑选出我们需要的元素,可以使用 nth()来进行挑选我们需要的是哪一个元素。索引是从 0 开始的。例如,如果我们想选择页面上第二个“公司名称”文本的元素,可以这样写:

const element = await page.get_by_text('公司名称', { exact: true }).nth(1);

3.first和last:选择第一个和最后一个元素

根据名称我们就可以知道,这是定位的第一个和最后一个元素,这两个是作为类属性使用的,使用时不需要加()在某些情况下,我们可能只关心一组元素中的第一个或最后一个。Playwright提供了first和last这两个类属性,用于快速选择第一个和最后一个元素。这两个属性无需加括号,直接作为方法调用即可。例如,如果我们想选择页面上第一个名为“确定”的按钮,可以这样写:

const button = await page.get_by_role('button', { name: '确定' }).first();

4.filter():二次筛选元素

根据名称我们就可以知道,这个是用来做筛选的。他的作用主要是在元素定位后,进行二次筛选。有利于在复杂的页面当中,过滤出我们需要的元素。主要用到的参数有两个,has_text: 包含的文本信息 has_not_text: 不包含的文本信息。

在复杂的网页中,有时我们需要通过多个条件来筛选元素。Playwright的filter()方法允许我们在元素定位后进行二次筛选。这使得我们能够在已经定位到的元素集合中,根据特定条件过滤出我们真正需要的元素。例如,如果我们想选择页面上所有带有“active”类的按钮中的第一个,可以这样写:

const activeButton = await page.get_by_role('button').filter(button => button.hasClass('active')).first();

5.链式选择器

我们先来认识一下链式选择器中的两个符号,常用的是 >>

1.>: 定位子元素,定位和父级元素相邻的元素,只能定位“亲儿子”
2.>>:定位后代元素,定位父级元素下的所有元素,只要位于父元素下,都可以定位
链式选择器用来根据多个 css 样式定位元素。当元素没有 id 并且 css 样式又繁多的时候,我们可以通过使用链式选择器,来根据多个 css 样式进行元素定位。例如,如果我们想定位 van-popover__wrapper 样式下样式为 MPMicon 的元素,可以这样写:

const = await page.locator('.van-popover__wrapper >> .MPMicon');

6.正则表达式

我们在根据文本信息进行元素定位时,有文本的部分内容会发生变化的情况,我们可以通过正则表达式,来根据某些固定的内容,进行元素定位。首先需要先了解一下
正则表达式的知识
例如,如果我们想定位名称由1-9数字开头和“ 个 进行中” 文字结尾的按钮,可以这样写:

const = await page.get_by_role("button", name=re.compile(r"[1-9]\d* 个 进行中$"));

7.XPath

XPath 是一种用于在 XML 文档中定位和选择节点的语言。它可以通过使用路径表达式来指定节点的位置,并支持使用各种条件进行过滤和匹配。以下是一些常见的 XPath 高阶定位方法:

  1. 使用逻辑运算符,如 and、or、not,将多个条件组合起来进行定位。
  2. 使用轴定位,通过预定义的轴(如子节点、父节点、兄弟节点等)来获取相对于当前节点的其他节点集合。
  3. 使用谓词,查找特定节点或包含特定值的节点,谓词嵌入方括号中。
  4. 使用内置函数,执行一些复杂的操作,如字符串处理、数值计算等。

7.1包含-contains()

  • Xpath 表达式中的一个函数,contains 会匹配符合某属性中包含 xx 字符串的元素。例如//*[contains(@text,"hogwarts")]则会匹配text属性的属性值中包含hogwarts的元素
  • contains()函数的使用格式
//*[contains(@属性,"属性值")]
  • 特点
    • contains() 函数定位的元素很容易为 list
    • contains() 函数内的属性名需要用 @ 开始

7.2XPath 轴

XPath 轴是 XPath 语言中的一个重要概念,它可以根据节点之间的关系来选择节点。XPath 轴定义了节点的一个集合,这个集合由满足特定条件的节点组成。

可以通过过定位一个节点,定位到当前的节点的兄弟节点、父节点、爷爷节点、祖先节点等等。

7.3XPath 运算符

7.3.1 AND

AND 表示可以在 XPath 表达式中同时具备 2 个条件,在 AND 两个条件都应该为真的情况下,即该元素既有 条件A 又有 条件B 。AND 定位取到的是交集。

示例:定位如下图页面中的红框所框出来的元素。demo网站:
https://sahitest.com/demo/formTest.htm

1.使用 type 属性进行定位时,会定位到多个元素(从图中看到定位到8个),如下图所示:

2.使用and运算符增加筛选条件进行过滤,需要满足符合 type 属性 ,且 name 内容为 name 的元素。只有两个条件都符合时才会被选中,如下图所示:

7.3.2 OR

OR 表示可以在 XPath 表达式中放置 2 个条件,在 OR 的情况下,两个条件中的任何一个为真,就可定位到该元素。OR 定位获取的是并集。

示例:定位当前页面中 type 为 text 或 name为 q 的元素,也就是下面 5 个元素。demo网站:
https://sahitest.com/demo/strict_visible.htm

Xpath语法:

//*[@type="text" or @name="q"]

7.4Xpath 混合使用

特性就是管道符
|
的使用,在XPath中可指定多个选择器。它将匹配该列表中的选择器之一可以选择的所有元素。

示例:定位当前页面中 type 为 text 或 name为 q 的元素,也就是下面 5 个元素。demo网站:
https://sahitest.com/demo/strict_visible.htm

Xpath语法:

//*[@type="text"] | //*[@name="q"]

7.5属性与逻辑定位

在前面我们介绍到使用属性定位 ,但是如若使用一个属性定位不到怎么办 ? 你就可以是用两个属性或者多个属性同时定位 。

这里就不得不说的一个逻辑运算符 ,and(逻辑与) . 它的意思是并且,大白话就是两者都要求满足 。比如 属性1 and 属性2 ,代表这两个属性都要同时都满足 。

所以 ,如果你一个属性定位不到的话 ,再加一个属性就可以进一步缩小范围,从而提高定位准确率 。

而这种写法也正好是xpath语言中所支持的,它的编写格式为 ://标签[@属性1='值1' and @属性2='值2'] 。

举例 :

  • xpath两个属性的编写格式 :
    //input[@class='text_cmu' and @name='username']
  • selenium xpath方法编写格式 :
    find_element_by_xpath("//input[@class='text_cmu' and @name='username']")

以上的定位虽然使用到了and逻辑运算符 ,但是xpath中,其实并不仅仅支持这一个逻辑运算符 。以下的都可以使用 :

  • 算术运算符 : = ,!= , < , <= , >, >=
  • 逻辑运算符 : or , and

只是以上运算符中,用在定位上的可能只有and比较有用 。

7.6路径与属性结合定位

如果你使用了上面的各种方法 ,依然定位不到元素 ,那这个时候 ,你就可以考虑把路径加进来 。

一般原则是先加它的父路径 ,然后再加上当前路径 ,结合使用 。

具体格式为 :

  • //*[@id='um']/input
    : 父路径属性 + 子标签
  • //bookstore/[@price='30']
    : 父路径标签 + 子属性
  • //div[@class='login_bnt']/a[@class='J-login-submit'
    : 父路径属性 + 子属性

不管咋写 ,只要能确定元素的唯一性 ,就都可以 ,不过这种写法很明显是逼不得已 ,因为你可能使用其它方法都无效的情况下 ,才会使用这种方法 。

8.趁热打铁

需求:

使用Java语言通过playwright完成对百度搜索的“北京-宏哥”的操作,具体如下 :

1.使用xpath属性定位百度首页输入框 ,并输入搜索内容:北京-宏哥,

2.使用路径与属性结合定位“百度一下”按钮,并点击 。

8.1代码设计

8.2参考代码

packagecom.bjhg.playwright;importcom.microsoft.playwright.Browser;importcom.microsoft.playwright.BrowserContext;importcom.microsoft.playwright.BrowserType;importcom.microsoft.playwright.Locator;importcom.microsoft.playwright.Page;importcom.microsoft.playwright.Playwright;/***@author北京-宏哥
*
* @公众号:北京宏哥(微信搜索,关注宏哥,提前解锁更多测试干货)
*
* 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-8- 元素高级定位技巧(详细教程)
*
* 2024年8月10日
*/ public classTest_Search {public static voidmain(String[] args) {try (Playwright playwright =Playwright.create()) {//使用chromium浏览器,# 浏览器配置,设置以GUI模式启动Chrome浏览器(要查看浏览器UI,在启动浏览器时传递 headless=false 标志。您还可以使用 slowMo 来减慢执行速度。 Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setSlowMo(500));
BrowserContext context
=browser.newContext();//创建page Page page =context.newPage();//浏览器打开百度 page.navigate("https://www.baidu.com/");//判断title是不是 百度一下,你就知道 try{
String baidu_title
= "百度一下,你就知道";assert baidu_title ==page.title();
System.out.println(
"Test Pass");

}
catch(Exception e){
e.printStackTrace();
}
//使用xpath属性定位百度首页输入框 ,并输入搜索内容:北京-宏哥 page.locator("//*[@id='kw']").type("北京-宏哥");//使用路径与属性结合定位“百度一下”按钮,并点击 。 page.locator("//span/input[@id='su']").click();//关闭page page.close();//关闭browser browser.close();
}
}

}

8.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作。如下图所示:

9.小结

9.1Xpath定位总结

我们将Xpath所有方法(基础+高级)总结为,可以使用以下的几种方法进行定位 。

定位方式 xpath
id属性定位 //*[@id='值']
class属性定位 //*[@class='值']
属性定位 //*[@属性名='值']
标签+属性定位 //标签[@属性名='值']
逻辑+属性定位 //标签[@属性名='值' and @属性名1='值1']
路径定位+属性定位 //标签[@属性名='值']/标签[@属性名='值']

Playwright提供了丰富多样的元素定位方式,无论是基础定位还是高级定位技巧,都能满足我们在自动化测试中的需求。掌握这些定位方式,将使我们能够更加灵活、高效地进行网页自动化测试。希望本文能够帮助读者更好地理解和应用Playwright的元素定位技术。

好了,今天时间也不早了,宏哥就讲解和分享到这里,感谢您耐心的阅读,希望对您有所帮助。

最后,首先宏哥要拉一下票,希望喜欢宏哥的支持一下,投下你宝贵的6票,投票完成还可以抽奖哦,灰常感谢!!!掘金2024年度人气创作者打榜中,快来帮我打榜吧

activity.juejin.cn/rank/2024/w…

钉钉机器人 自动化发版

#1 简介

  • 开发机器人接收消息并调用构建接口, 实现自动化发版
  • 发送指令 -> 机器人接收指令 -> 调用jenkins-job远程构建与部署
  • jenkins配置,勾选job配置的
    触发远程构建
    并设置
    身份验证令牌
#测试 触发远程构建
curl -ks -u user:user_token -X POST \
  jenkins_url/job/job_name/buildWithParameters?token=job_token


#2、创建机器人

#2.1 登录钉钉开放平台
#2.2 创建机器人
  • 应用开发 -> 机器人 -> 创建应用
    • 继续使用旧版,名称如
      cici
    • 应用信息,复制
      AppSecret
  • 开发管理,修改,消息接收地址

  • 创建test企业群, 添加机器人cici
  • 复制群机器人token到default_token
#2.3 运行机器人服务

配置环境变量文件.env_lark

#vim .env_dingtalk 
# 钉钉机器人密钥 AppSecret
ding_secret=Q-uG5AMlMgC_Tkn6qhz1601xMYfQgxzeQh3xxx
#默认 机器人token
ding_webhook_default_token=bf5ab6a77cbc1b7c21fcxxx
#jenkins
JenkinsBaseUrl=https://user:user_token@jenkins.elvin.vip/job/

使用docker启动机器人服务

docker rm -f robot-dingtalk &>/dev/null
docker run -dit --name robot-dingtalk \
 --restart=always -h robot-dingtalk --net=host\
 -v $Dir:/opt --env-file .env_dingtalk \
registry.aliyuncs.com/elvin/python:dingtalk-robot \
python3 /opt/dingtalk-robot.py

dingtalk-robot.py
实例在https://gitee.com/alivv/elvin-demo

nignx配置域名和lark反向代理

#dingtalk-cicd
location ~ ^/dingtalk-cicd {
    proxy_pass http://127.0.0.1:8091;
    proxy_set_header Host $host:$server_port;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}


#3 发送消息测试


#3 源码

python实例如下:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# By Elvin , blog.elvin.vip

import os
import time
import hmac
import hashlib
import base64
import json
from datetime import datetime
from flask import Flask, request
import requests

# 从环境变量加载配置
ding_secret = os.getenv("ding_secret")
ding_webhook_default_token = os.getenv("ding_webhook_default_token")

app = Flask(__name__)

#钉钉发送文本消息
def send_txt_msg(message, webhook_url):
    data = { "msgtype": "text","text": {"content": message}}
    #requests发送post请求
    req = requests.post(webhook_url, json=data)
    print(req)

#签名核对
def check_sign(timestamp=int(time.time() * 1000), app_secret=ding_secret):
    #钉钉消息头部加密
    app_secret_enc = app_secret.encode('utf-8')
    string_to_sign = '{}\n{}'.format(timestamp, app_secret)
    string_to_sign_enc = string_to_sign.encode('utf-8')
    hmac_code = hmac.new(app_secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
    sign = base64.b64encode(hmac_code).decode('utf-8')
    return sign


# Default route, print user's IP
@app.route('/')
def remoteIP():
    if 'X-Forwarded-For' in request.headers:
        ip = request.headers['X-Forwarded-For'].split(',')[0]
    else:
        ip = request.remote_addr
    return ip + "\n", 200, [("Server", "Go"), ("City", "Shanghai")]


# 接收@机器人的消息
@app.route('/dingtalk-cicd', methods=["POST"])
def index():
    if request.method == "POST":
        timestamp = request.headers.get('Timestamp')
        sign = request.headers.get('Sign')
        if check_sign(timestamp=timestamp) == sign:
            req_data = json.loads(str(request.data, 'utf-8'))
            sender = req_data.get('senderNick')
            text = req_data.get('text').get('content', "").strip()
            ddgroup = req_data.get('conversationTitle').strip()
            #msg
            msg_cicd(ddgroup,text,sender)
            return "succeed"
        else:
            return "not found"
    else:
        return "method not found"


#筛选消息,执行指令
def msg_cicd(ddgroup,text,sender):
    msg = text
    sender = sender
    ddGroup = ddgroup
    print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), " msg->: ",  msg)
    #check ddgroup
    if ddGroup == "test" or ddGroup == "DevOps":
        webhook = ding_webhook_test
        appInfoMap = dict(appTest, **appProd)
        myMenu = {"help", "test", "prod"}
        if msg in appInfoMap:
            app_env = appInfoMap[msg][0]
            app_name = appInfoMap[msg][1]
            app_url = appInfoMap[msg][2]
            #app_url = appInfoMap[msg][2] + appInfoMap[msg][1]
            app_url = app_url + app_env + "&app_list=" + app_name
            if app_env != "":
                #执行通知
                msg = "By:  %s\nenv:  %s\napp:  %s" % (sender, app_env, app_name)
                send_txt_msg(msg,webhook)
                head = { 'User-Agent': "webhook-dingtalk-robot" }
                #向webhook发起post请求
                res = requests.post(url=app_url, headers=head)
                print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "run", app_env, app_name, res.reason)
                return "succeed"
            else:
                print(msg, "nothing")
                return "succeed"
        elif msg in myMenu:
            #打印命令列表
            print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "print menu ", msg)
            msgTitle = "#命令  名称\n"
            if msg == "help":
                msgTitle2 = "#命令  获取列表\n"
                msg = msgTitle2 + "test  app-test-list\nprod  app-prod-list"
            elif msg == "test":
                msg = msgTitle
                for i in appTest:
                    msg = msg + i + "  " + appInfoMap[i][1] + "\n"
            elif msg == "prod":
                msg = msgTitle
                for i in appProd:
                    msg = msg + i + "  " + appInfoMap[i][1] + "\n"
            msg = msg.rstrip('\n')
            send_txt_msg(msg,webhook)
            return "succeed"
        else:
            msg = f"已收到: {msg} \n发送 help@cici 查看支持指令"
            send_txt_msg(msg,webhook)
            return "succeed"

    else:
        print(datetime.now().strftime('%Y-%m-%d %H:%M:%S')," no ddGroup config for ",ddGroup)
        webhook = ding_webhook_default
        msg = f"已收到: {msg} \n未发现组{ddGroup}支持指令"
        send_txt_msg(msg,webhook)
        return "succeed"


#webhook
ding_webhook_base_url = "https://oapi.dingtalk.com/robot/send?access_token="
ding_webhook_default = ding_webhook_base_url + ding_webhook_default_token

ding_webhook_test = ding_webhook_default

#webhook url for jenkins 
JenkinsBaseUrl = os.getenv("JenkinsBaseUrl")

#job
appDeploy = "test-app-deploy/buildWithParameters?token=cicdTest&app_branch=master&app_build=true&docker_build=true&create_git_tag=false&notice_msg=true&app_deploy=true&image_update=true&input_pass=true&deploy_tag=tag&deploy_env="

#ci url
appDeployUrl = JenkinsBaseUrl + appDeploy

#hybrid list
appTest = {
"#app-test-k8s-list:": ["","", ""],
"s201": ["test","app-web", appDeployUrl],
"s202": ["test","app-svc", appDeployUrl],
"s203": ["test","app-api", appDeployUrl],
"s204": ["test","app-event", appDeployUrl],
"s205": ["test","app-admin", appDeployUrl],
}

appProd = {
"#app-prod-k8s-list:": ["","", ""],
"s101": ["prod","app-web", appDeployUrl],
"s102": ["prod","app-svc", appDeployUrl],
"s103": ["prod","app-api", appDeployUrl],
"s104": ["prod","app-event", appDeployUrl],
"s105": ["prod","app-admin", appDeployUrl],
}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8091)

source:
https://gitee.com/alivv/elvin-demo